/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.trino.plugin.geospatial;

import com.esri.core.geometry.Point;
import com.esri.core.geometry.ogc.OGCGeometry;
import com.esri.core.geometry.ogc.OGCPoint;
import com.google.common.collect.ImmutableList;
import io.trino.geospatial.KdbTreeUtils;
import io.trino.geospatial.Rectangle;
import io.trino.geospatial.serde.GeometrySerde;
import io.trino.spi.block.Block;
import io.trino.spi.block.BlockBuilder;
import io.trino.spi.type.ArrayType;
import io.trino.spi.type.RowType;
import io.trino.sql.query.QueryAssertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.parallel.Execution;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static io.trino.geospatial.KdbTree.buildKdbTree;
import static io.trino.plugin.geospatial.GeoFunctions.stCentroid;
import static io.trino.plugin.geospatial.GeometryType.GEOMETRY;
import static io.trino.spi.type.BooleanType.BOOLEAN;
import static io.trino.spi.type.DoubleType.DOUBLE;
import static io.trino.spi.type.IntegerType.INTEGER;
import static io.trino.spi.type.VarcharType.VARCHAR;
import static io.trino.testing.assertions.TrinoExceptionAssert.assertTrinoExceptionThrownBy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;

@TestInstance(PER_CLASS)
@Execution(CONCURRENT)
public class TestGeoFunctions
{
    private QueryAssertions assertions;

    @BeforeAll
    public void init()
    {
        assertions = new QueryAssertions();
        assertions.addPlugin(new GeoPlugin());
    }

    @AfterAll
    public void teardown()
    {
        assertions.close();
        assertions = null;
    }

    @Test
    public void testSpatialPartitions()
    {
        String kdbTreeJson = makeKdbTreeJson();

        assertSpatialPartitions(kdbTreeJson, "POINT EMPTY", null);
        // points inside partitions
        assertSpatialPartitions(kdbTreeJson, "POINT (0 0)", ImmutableList.of(0));
        assertSpatialPartitions(kdbTreeJson, "POINT (3 1)", ImmutableList.of(2));
        // point on the border between two partitions
        assertSpatialPartitions(kdbTreeJson, "POINT (1 2.5)", ImmutableList.of(1));
        // point at the corner of three partitions
        assertSpatialPartitions(kdbTreeJson, "POINT (4.5 2.5)", ImmutableList.of(4));
        // points outside
        assertSpatialPartitions(kdbTreeJson, "POINT (2 6)", ImmutableList.of());
        assertSpatialPartitions(kdbTreeJson, "POINT (3 -1)", ImmutableList.of());
        assertSpatialPartitions(kdbTreeJson, "POINT (10 3)", ImmutableList.of());

        // geometry within a partition
        assertSpatialPartitions(kdbTreeJson, "MULTIPOINT (5 0.1, 6 2)", ImmutableList.of(3));
        // geometries spanning multiple partitions
        assertSpatialPartitions(kdbTreeJson, "MULTIPOINT (5 0.1, 5.5 3, 6 2)", ImmutableList.of(3, 4));
        assertSpatialPartitions(kdbTreeJson, "MULTIPOINT (3 2, 8 3)", ImmutableList.of(2, 3, 4, 5));
        // geometry outside
        assertSpatialPartitions(kdbTreeJson, "MULTIPOINT (2 6, 3 7)", ImmutableList.of());

        // with distance
        assertSpatialPartitions(kdbTreeJson, "POINT EMPTY", 1.2, null);
        assertSpatialPartitions(kdbTreeJson, "POINT (1 1)", 1.2, ImmutableList.of(0));
        assertSpatialPartitions(kdbTreeJson, "POINT (1 1)", 2.3, ImmutableList.of(0, 1, 2));
        assertSpatialPartitions(kdbTreeJson, "MULTIPOINT (5 0.1, 6 2)", 0.2, ImmutableList.of(3));
        assertSpatialPartitions(kdbTreeJson, "MULTIPOINT (5 0.1, 6 2)", 1.2, ImmutableList.of(2, 3, 4));
        assertSpatialPartitions(kdbTreeJson, "MULTIPOINT (2 6, 3 7)", 1.2, ImmutableList.of());
    }

    private static String makeKdbTreeJson()
    {
        ImmutableList.Builder<Rectangle> rectangles = ImmutableList.builder();
        for (double x = 0; x < 10; x += 1) {
            for (double y = 0; y < 5; y += 1) {
                rectangles.add(new Rectangle(x, y, x + 1, y + 2));
            }
        }
        return KdbTreeUtils.toJson(buildKdbTree(10, new Rectangle(0, 0, 9, 4), rectangles.build())).toStringUtf8();
    }

    private void assertSpatialPartitions(String kdbTreeJson, String wkt, List<Integer> expectedPartitions)
    {
        assertThat(assertions.function("spatial_partitions", "cast('%s' as KdbTree)".formatted(kdbTreeJson), "ST_GeometryFromText('%s')".formatted(wkt)))
                .hasType(new ArrayType(INTEGER))
                .isEqualTo(expectedPartitions);
    }

    private void assertSpatialPartitions(String kdbTreeJson, String wkt, double distance, List<Integer> expectedPartitions)
    {
        assertThat(assertions.function("spatial_partitions", "cast('%s' as KdbTree)".formatted(kdbTreeJson), "ST_GeometryFromText('%s')".formatted(wkt), Double.toString(distance)))
                .hasType(new ArrayType(INTEGER))
                .isEqualTo(expectedPartitions);
    }

    @Test
    public void testGeometryGetObjectValue()
    {
        BlockBuilder builder = GEOMETRY.createBlockBuilder(null, 1);
        GEOMETRY.writeSlice(builder, GeoFunctions.stPoint(1.2, 3.4));
        Block block = builder.build();

        assertThat("POINT (1.2 3.4)").isEqualTo(GEOMETRY.getObjectValue(block, 0));
    }

    @Test
    public void testSTPoint()
    {
        assertThat(assertions.function("ST_AsText", "ST_Point(1, 4)"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (1 4)");

        assertThat(assertions.function("ST_AsText", "ST_Point(122.3, 10.55)"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (122.3 10.55)");
    }

    @Test
    public void testSTLineFromText()
    {
        assertThat(assertions.function("ST_AsText", "ST_LineFromText('LINESTRING EMPTY')"))
                .hasType(VARCHAR)
                .isEqualTo("LINESTRING EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_LineFromText('LINESTRING (1 1, 2 2, 1 3)')"))
                .hasType(VARCHAR)
                .isEqualTo("LINESTRING (1 1, 2 2, 1 3)");

        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "ST_LineFromText('MULTILINESTRING EMPTY')")::evaluate)
                .hasMessage("ST_LineFromText only applies to LINE_STRING. Input type is: MULTI_LINE_STRING");

        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "ST_LineFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')")::evaluate)
                .hasMessage("ST_LineFromText only applies to LINE_STRING. Input type is: POLYGON");
    }

    @Test
    public void testSTPolygon()
    {
        assertThat(assertions.function("ST_AsText", "ST_Polygon('POLYGON EMPTY')"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Polygon('POLYGON ((1 1, 1 4, 4 4, 4 1))')"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 1, 4 1, 4 4, 1 4, 1 1))");

        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "ST_Polygon('LINESTRING (1 1, 2 2, 1 3)')")::evaluate)
                .hasMessage("ST_Polygon only applies to POLYGON. Input type is: LINE_STRING");
    }

    @Test
    public void testSTArea()
    {
        assertArea("POLYGON ((2 2, 2 6, 6 6, 6 2))", 16.0);
        assertArea("POLYGON EMPTY", 0.0);
        assertArea("LINESTRING (1 4, 2 5)", 0.0);
        assertArea("LINESTRING EMPTY", 0.0);
        assertArea("POINT (1 4)", 0.0);
        assertArea("POINT EMPTY", 0.0);
        assertArea("GEOMETRYCOLLECTION EMPTY", 0.0);

        // Test basic geometry collection. Area is the area of the polygon.
        assertArea("GEOMETRYCOLLECTION (POINT (8 8), LINESTRING (5 5, 6 6), POLYGON ((1 1, 3 1, 3 4, 1 4, 1 1)))", 6.0);

        // Test overlapping geometries. Area is the sum of the individual elements
        assertArea("GEOMETRYCOLLECTION (POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)), POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1)))", 8.0);

        // Test nested geometry collection
        assertArea("GEOMETRYCOLLECTION (POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)), POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1)), GEOMETRYCOLLECTION (POINT (8 8), LINESTRING (5 5, 6 6), POLYGON ((1 1, 3 1, 3 4, 1 4, 1 1))))", 14.0);
    }

    private void assertArea(String wkt, double expectedArea)
    {
        assertThat(assertions.expression("ST_Area(geometry)")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt)))
                .isEqualTo(expectedArea);
    }

    @Test
    public void testSTBuffer()
    {
        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_Point(0, 0), 0.5)"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((0.5 0, 0.4989294616193014 0.03270156461507146, 0.49572243068690486 0.0652630961100257, 0.4903926402016149 0.09754516100806403, 0.4829629131445338 0.12940952255126026, 0.47346506474755257 0.16071973265158065, 0.46193976625564315 0.19134171618254472, 0.4484363707663439 0.22114434510950046, 0.43301270189221913 0.2499999999999998, 0.41573480615127245 0.2777851165098009, 0.39667667014561747 0.30438071450436016, 0.3759199037394886 0.32967290755003426, 0.3535533905932737 0.3535533905932736, 0.32967290755003437 0.3759199037394886, 0.3043807145043603 0.39667667014561747, 0.2777851165098011 0.4157348061512725, 0.24999999999999997 0.43301270189221924, 0.22114434510950062 0.4484363707663441, 0.19134171618254486 0.4619397662556433, 0.16071973265158077 0.4734650647475528, 0.12940952255126037 0.48296291314453416, 0.09754516100806412 0.4903926402016152, 0.06526309611002579 0.4957224306869052, 0.03270156461507153 0.49892946161930174, 0 0.5, -0.03270156461507146 0.4989294616193014, -0.0652630961100257 0.49572243068690486, -0.09754516100806403 0.4903926402016149, -0.12940952255126026 0.4829629131445338, -0.16071973265158065 0.47346506474755257, -0.19134171618254472 0.46193976625564315, -0.22114434510950046 0.4484363707663439, -0.2499999999999998 0.43301270189221913, -0.2777851165098009 0.41573480615127245, -0.30438071450436016 0.39667667014561747, -0.32967290755003426 0.3759199037394886, -0.3535533905932736 0.3535533905932737, -0.3759199037394886 0.32967290755003437, -0.39667667014561747 0.3043807145043603, -0.4157348061512725 0.2777851165098011, -0.43301270189221924 0.24999999999999997, -0.4484363707663441 0.22114434510950062, -0.4619397662556433 0.19134171618254486, -0.4734650647475528 0.16071973265158077, -0.48296291314453416 0.12940952255126037, -0.4903926402016152 0.09754516100806412, -0.4957224306869052 0.06526309611002579, -0.49892946161930174 0.03270156461507153, -0.5 0, -0.4989294616193014 -0.03270156461507146, -0.49572243068690486 -0.0652630961100257, -0.4903926402016149 -0.09754516100806403, -0.4829629131445338 -0.12940952255126026, -0.47346506474755257 -0.16071973265158065, -0.46193976625564315 -0.19134171618254472, -0.4484363707663439 -0.22114434510950046, -0.43301270189221913 -0.2499999999999998, -0.41573480615127245 -0.2777851165098009, -0.39667667014561747 -0.30438071450436016, -0.3759199037394886 -0.32967290755003426, -0.3535533905932737 -0.3535533905932736, -0.32967290755003437 -0.3759199037394886, -0.3043807145043603 -0.39667667014561747, -0.2777851165098011 -0.4157348061512725, -0.24999999999999997 -0.43301270189221924, -0.22114434510950062 -0.4484363707663441, -0.19134171618254486 -0.4619397662556433, -0.16071973265158077 -0.4734650647475528, -0.12940952255126037 -0.48296291314453416, -0.09754516100806412 -0.4903926402016152, -0.06526309611002579 -0.4957224306869052, -0.03270156461507153 -0.49892946161930174, 0 -0.5, 0.03270156461507146 -0.4989294616193014, 0.0652630961100257 -0.49572243068690486, 0.09754516100806403 -0.4903926402016149, 0.12940952255126026 -0.4829629131445338, 0.16071973265158065 -0.47346506474755257, 0.19134171618254472 -0.46193976625564315, 0.22114434510950046 -0.4484363707663439, 0.2499999999999998 -0.43301270189221913, 0.2777851165098009 -0.41573480615127245, 0.30438071450436016 -0.39667667014561747, 0.32967290755003426 -0.3759199037394886, 0.3535533905932736 -0.3535533905932737, 0.3759199037394886 -0.32967290755003437, 0.39667667014561747 -0.3043807145043603, 0.4157348061512725 -0.2777851165098011, 0.43301270189221924 -0.24999999999999997, 0.4484363707663441 -0.22114434510950062, 0.4619397662556433 -0.19134171618254486, 0.4734650647475528 -0.16071973265158077, 0.48296291314453416 -0.12940952255126037, 0.4903926402016152 -0.09754516100806412, 0.4957224306869052 -0.06526309611002579, 0.49892946161930174 -0.03270156461507153, 0.5 0))");

        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_LineFromText('LINESTRING (0 0, 1 1, 2 0.5)'), 0.2)"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((0 -0.19999999999999996, 0.013080625846028537 -0.19957178464772052, 0.02610523844401036 -0.19828897227476194, 0.03901806440322564 -0.19615705608064593, 0.05176380902050415 -0.1931851652578136, 0.06428789306063232 -0.18938602589902098, 0.07653668647301792 -0.18477590650225728, 0.0884577380438003 -0.17937454830653754, 0.09999999999999987 -0.17320508075688767, 0.11111404660392044 -0.166293922460509, 0.12175228580174413 -0.15867066805824703, 0.13186916302001372 -0.15036796149579545, 0.14142135623730945 -0.14142135623730945, 1.0394906098164265 0.7566478973418078, 1.9105572809000084 0.32111456180001685, 1.9115422619561997 0.32062545169346235, 1.923463313526982 0.31522409349774266, 1.9357121069393677 0.3106139741009789, 1.9482361909794959 0.3068148347421863, 1.9609819355967744 0.3038429439193539, 1.9738947615559896 0.30171102772523795, 1.9869193741539715 0.30042821535227926, 2 0.3, 2.0130806258460288 0.3004282153522794, 2.02610523844401 0.30171102772523806, 2.0390180644032254 0.30384294391935407, 2.051763809020504 0.30681483474218646, 2.0642878930606323 0.31061397410097896, 2.076536686473018 0.3152240934977427, 2.0884577380438003 0.32062545169346246, 2.1 0.3267949192431123, 2.1111140466039204 0.333706077539491, 2.121752285801744 0.34132933194175297, 2.1318691630200135 0.34963203850420455, 2.1414213562373092 0.35857864376269055, 2.1503679614957956 0.3681308369799863, 2.158670668058247 0.37824771419825587, 2.166293922460509 0.38888595339607956, 2.1732050807568877 0.4, 2.1793745483065377 0.41154226195619975, 2.1847759065022574 0.4234633135269821, 2.189386025899021 0.4357121069393677, 2.193185165257814 0.44823619097949585, 2.1961570560806463 0.46098193559677436, 2.1982889722747623 0.4738947615559897, 2.1995717846477207 0.4869193741539714, 2.2 0.5, 2.1995717846477207 0.5130806258460285, 2.198288972274762 0.5261052384440102, 2.196157056080646 0.5390180644032256, 2.1931851652578134 0.5517638090205041, 2.189386025899021 0.5642878930606323, 2.1847759065022574 0.5765366864730179, 2.1793745483065377 0.5884577380438002, 2.1732050807568877 0.5999999999999999, 2.166293922460509 0.6111140466039204, 2.158670668058247 0.6217522858017441, 2.1503679614957956 0.6318691630200137, 2.1414213562373097 0.6414213562373094, 2.131869163020014 0.6503679614957955, 2.121752285801744 0.658670668058247, 2.1111140466039204 0.666293922460509, 2.1 0.6732050807568877, 2.0894427190999916 0.6788854381999831, 1.0894427190999916 1.1788854381999831, 1.0884577380438003 1.1793745483065377, 1.076536686473018 1.1847759065022574, 1.0642878930606323 1.189386025899021, 1.0517638090205041 1.1931851652578138, 1.0390180644032256 1.196157056080646, 1.0261052384440104 1.198288972274762, 1.0130806258460288 1.1995717846477207, 1 1.2, 0.9869193741539715 1.1995717846477205, 0.9738947615559896 1.1982889722747618, 0.9609819355967744 1.1961570560806458, 0.9482361909794959 1.1931851652578136, 0.9357121069393677 1.189386025899021, 0.9234633135269821 1.1847759065022574, 0.9115422619561997 1.1793745483065377, 0.9000000000000001 1.1732050807568877, 0.8888859533960796 1.166293922460509, 0.8782477141982559 1.158670668058247, 0.8681308369799863 1.1503679614957956, 0.8585786437626906 1.1414213562373094, -0.14142135623730967 0.1414213562373095, -0.15036796149579557 0.13186916302001372, -0.1586706680582468 0.12175228580174413, -0.1662939224605089 0.11111404660392044, -0.17320508075688767 0.09999999999999998, -0.17937454830653765 0.08845773804380025, -0.1847759065022574 0.07653668647301792, -0.18938602589902098 0.06428789306063232, -0.19318516525781382 0.05176380902050415, -0.19615705608064626 0.03901806440322564, -0.19828897227476228 0.026105238444010304, -0.19957178464772074 0.013080625846028593, -0.20000000000000018 0, -0.19957178464772074 -0.013080625846028537, -0.19828897227476183 -0.026105238444010248, -0.19615705608064582 -0.03901806440322564, -0.19318516525781337 -0.05176380902050415, -0.18938602589902098 -0.06428789306063232, -0.1847759065022574 -0.07653668647301792, -0.17937454830653765 -0.0884577380438002, -0.17320508075688767 -0.09999999999999987, -0.1662939224605089 -0.11111404660392044, -0.1586706680582468 -0.12175228580174413, -0.15036796149579557 -0.13186916302001372, -0.14142135623730967 -0.14142135623730945, -0.13186916302001395 -0.15036796149579545, -0.12175228580174391 -0.15867066805824703, -0.11111404660392044 -0.166293922460509, -0.10000000000000009 -0.17320508075688767, -0.0884577380438003 -0.17937454830653765, -0.07653668647301792 -0.1847759065022574, -0.06428789306063232 -0.1893860258990211, -0.05176380902050415 -0.1931851652578137, -0.03901806440322586 -0.19615705608064604, -0.026105238444010137 -0.19828897227476205, -0.01308062584602876 -0.19957178464772074, 0 -0.19999999999999996))");

        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_GeometryFromText('POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))'), 1.2)"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((-1.2 0, -1.1974307078863233 -0.0784837550761715, -1.1897338336485717 -0.15663143066406168, -1.1769423364838756 -0.23410838641935366, -1.1591109915468811 -0.3105828541230246, -1.1363161553941261 -0.38572735836379357, -1.1086554390135435 -0.4592201188381073, -1.0762472898392252 -0.530746428262801, -1.0392304845413258 -0.5999999999999995, -0.9977635347630538 -0.6666842796235222, -0.9520240083494819 -0.7305137148104643, -0.9022077689747725 -0.7912149781200822, -0.8485281374238568 -0.8485281374238567, -0.7912149781200825 -0.9022077689747725, -0.7305137148104647 -0.9520240083494819, -0.6666842796235226 -0.997763534763054, -0.5999999999999999 -1.039230484541326, -0.5307464282628015 -1.0762472898392257, -0.45922011883810765 -1.108655439013544, -0.38572735836379385 -1.1363161553941266, -0.3105828541230249 -1.159110991546882, -0.2341083864193539 -1.1769423364838765, -0.15663143066406188 -1.1897338336485723, -0.07848375507617167 -1.1974307078863242, 0 -1.2, 5 -1.2, 5.078483755076172 -1.1974307078863233, 5.156631430664062 -1.1897338336485717, 5.234108386419353 -1.1769423364838756, 5.310582854123025 -1.1591109915468811, 5.385727358363794 -1.1363161553941261, 5.4592201188381075 -1.1086554390135435, 5.530746428262801 -1.0762472898392252, 5.6 -1.0392304845413258, 5.666684279623523 -0.9977635347630538, 5.730513714810464 -0.9520240083494819, 5.791214978120082 -0.9022077689747725, 5.848528137423857 -0.8485281374238568, 5.9022077689747725 -0.7912149781200825, 5.952024008349482 -0.7305137148104647, 5.997763534763054 -0.6666842796235226, 6.039230484541326 -0.5999999999999999, 6.076247289839226 -0.5307464282628015, 6.108655439013544 -0.45922011883810765, 6.136316155394127 -0.38572735836379385, 6.159110991546882 -0.3105828541230249, 6.176942336483877 -0.2341083864193539, 6.189733833648573 -0.15663143066406188, 6.197430707886324 -0.07848375507617167, 6.2 0, 6.2 5, 6.1974307078863236 5.078483755076172, 6.189733833648572 5.156631430664062, 6.176942336483876 5.234108386419353, 6.159110991546881 5.310582854123025, 6.136316155394126 5.385727358363794, 6.1086554390135435 5.4592201188381075, 6.076247289839225 5.530746428262801, 6.039230484541326 5.6, 5.997763534763054 5.666684279623523, 5.952024008349482 5.730513714810464, 5.9022077689747725 5.791214978120082, 5.848528137423857 5.848528137423857, 5.791214978120083 5.9022077689747725, 5.730513714810464 5.952024008349482, 5.666684279623523 5.997763534763054, 5.6 6.039230484541326, 5.530746428262802 6.076247289839226, 5.4592201188381075 6.108655439013544, 5.385727358363794 6.136316155394127, 5.310582854123025 6.159110991546882, 5.234108386419354 6.176942336483877, 5.156631430664062 6.189733833648573, 5.078483755076172 6.197430707886324, 5 6.2, 0 6.2, -0.0784837550761715 6.1974307078863236, -0.15663143066406168 6.189733833648572, -0.23410838641935366 6.176942336483876, -0.3105828541230246 6.159110991546881, -0.38572735836379357 6.136316155394126, -0.4592201188381073 6.1086554390135435, -0.530746428262801 6.076247289839225, -0.5999999999999995 6.039230484541326, -0.6666842796235222 5.997763534763054, -0.7305137148104643 5.952024008349482, -0.7912149781200822 5.9022077689747725, -0.8485281374238567 5.848528137423857, -0.9022077689747725 5.791214978120083, -0.9520240083494819 5.730513714810464, -0.997763534763054 5.666684279623523, -1.039230484541326 5.6, -1.0762472898392257 5.530746428262802, -1.108655439013544 5.4592201188381075, -1.1363161553941266 5.385727358363794, -1.159110991546882 5.310582854123025, -1.1769423364838765 5.234108386419354, -1.1897338336485723 5.156631430664062, -1.1974307078863242 5.078483755076172, -1.2 5, -1.2 0))");

        // zero distance
        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_Point(0, 0), 0)"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (0 0)");

        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_LineFromText('LINESTRING (0 0, 1 1, 2 0.5)'), 0)"))
                .hasType(VARCHAR)
                .isEqualTo("LINESTRING (0 0, 1 1, 2 0.5)");

        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_GeometryFromText('POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))'), 0)"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))");

        // geometry collection
        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_Intersection(ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))'), ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))')), 0.2)"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOLYGON (((5 0.8, 5.013080625846029 0.8004282153522794, 5.026105238444011 0.801711027725238, 5.039018064403225 0.803842943919354, 5.051763809020504 0.8068148347421864, 5.064287893060633 0.8106139741009789, 5.076536686473018 0.8152240934977427, 5.0884577380438 0.8206254516934623, 5.1 0.8267949192431123, 5.11111404660392 0.833706077539491, 5.121752285801744 0.841329331941753, 5.1318691630200135 0.8496320385042045, 5.141421356237309 0.8585786437626906, 5.150367961495795 0.8681308369799863, 5.158670668058247 0.8782477141982559, 5.166293922460509 0.8888859533960796, 5.173205080756888 0.9, 5.179374548306538 0.9115422619561997, 5.184775906502257 0.9234633135269821, 5.189386025899021 0.9357121069393677, 5.193185165257813 0.9482361909794959, 5.196157056080646 0.9609819355967744, 5.198288972274762 0.9738947615559896, 5.199571784647721 0.9869193741539714, 5.2 1, 5.199571784647721 1.0130806258460288, 5.198288972274762 1.0261052384440104, 5.196157056080646 1.0390180644032256, 5.193185165257813 1.0517638090205041, 5.189386025899021 1.0642878930606323, 5.184775906502257 1.076536686473018, 5.179374548306537 1.0884577380438003, 5.173205080756888 1.1, 5.166293922460509 1.1111140466039204, 5.158670668058247 1.1217522858017441, 5.150367961495795 1.1318691630200137, 5.141421356237309 1.1414213562373094, 5.1318691630200135 1.1503679614957956, 5.121752285801744 1.158670668058247, 5.11111404660392 1.1662939224605091, 5.1 1.1732050807568877, 5.0884577380438 1.1793745483065377, 5.076536686473018 1.1847759065022574, 5.064287893060632 1.1893860258990212, 5.051763809020504 1.1931851652578138, 5.039018064403225 1.196157056080646, 5.026105238444011 1.198288972274762, 5.013080625846029 1.1995717846477207, 5 1.2, 4.986919374153971 1.1995717846477207, 4.973894761555989 1.198288972274762, 4.960981935596775 1.196157056080646, 4.948236190979496 1.1931851652578136, 4.935712106939367 1.189386025899021, 4.923463313526982 1.1847759065022574, 4.9115422619562 1.1793745483065377, 4.9 1.1732050807568877, 4.88888595339608 1.166293922460509, 4.878247714198256 1.158670668058247, 4.8681308369799865 1.1503679614957956, 4.858578643762691 1.1414213562373094, 4.849632038504205 1.1318691630200137, 4.841329331941753 1.1217522858017441, 4.833706077539491 1.1111140466039204, 4.826794919243112 1.1, 4.820625451693462 1.0884577380438003, 4.815224093497743 1.076536686473018, 4.810613974100979 1.0642878930606323, 4.806814834742187 1.0517638090205041, 4.803842943919354 1.0390180644032256, 4.801711027725238 1.0261052384440104, 4.800428215352279 1.0130806258460285, 4.8 1, 4.800428215352279 0.9869193741539714, 4.801711027725238 0.9738947615559896, 4.803842943919354 0.9609819355967743, 4.806814834742187 0.9482361909794959, 4.810613974100979 0.9357121069393677, 4.815224093497743 0.923463313526982, 4.820625451693463 0.9115422619561997, 4.826794919243112 0.8999999999999999, 4.833706077539491 0.8888859533960796, 4.841329331941753 0.8782477141982559, 4.849632038504205 0.8681308369799862, 4.858578643762691 0.8585786437626904, 4.8681308369799865 0.8496320385042044, 4.878247714198256 0.841329331941753, 4.88888595339608 0.8337060775394909, 4.9 0.8267949192431122, 4.9115422619562 0.8206254516934623, 4.923463313526982 0.8152240934977426, 4.935712106939368 0.8106139741009788, 4.948236190979496 0.8068148347421863, 4.960981935596775 0.8038429439193538, 4.973894761555989 0.801711027725238, 4.986919374153971 0.8004282153522793, 5 0.8)), ((3 3.8, 4 3.8, 4.013080625846029 3.8004282153522793, 4.026105238444011 3.801711027725238, 4.039018064403225 3.803842943919354, 4.051763809020504 3.8068148347421866, 4.064287893060632 3.810613974100979, 4.076536686473018 3.8152240934977426, 4.0884577380438 3.8206254516934623, 4.1 3.8267949192431123, 4.11111404660392 3.833706077539491, 4.121752285801744 3.841329331941753, 4.1318691630200135 3.8496320385042044, 4.141421356237309 3.8585786437626903, 4.150367961495795 3.868130836979986, 4.158670668058247 3.878247714198256, 4.166293922460509 3.8888859533960796, 4.173205080756888 3.9, 4.179374548306537 3.9115422619561997, 4.184775906502257 3.923463313526982, 4.189386025899021 3.9357121069393677, 4.193185165257813 3.948236190979496, 4.196157056080646 3.960981935596774, 4.198288972274762 3.97389476155599, 4.199571784647721 3.9869193741539712, 4.2 4, 4.199571784647721 4.013080625846029, 4.198288972274762 4.026105238444011, 4.196157056080646 4.039018064403225, 4.193185165257813 4.051763809020504, 4.189386025899021 4.064287893060632, 4.184775906502257 4.076536686473018, 4.179374548306537 4.0884577380438, 4.173205080756888 4.1, 4.166293922460509 4.11111404660392, 4.158670668058247 4.121752285801744, 4.150367961495795 4.1318691630200135, 4.141421356237309 4.141421356237309, 4.1318691630200135 4.150367961495795, 4.121752285801744 4.158670668058247, 4.11111404660392 4.166293922460509, 4.1 4.173205080756888, 4.0884577380438 4.179374548306537, 4.076536686473018 4.184775906502257, 4.064287893060632 4.189386025899021, 4.051763809020504 4.193185165257813, 4.039018064403225 4.196157056080646, 4.026105238444011 4.198288972274762, 4.013080625846029 4.199571784647721, 4 4.2, 3 4.2, 2.9869193741539712 4.199571784647721, 2.9738947615559894 4.198288972274762, 2.9609819355967746 4.196157056080646, 2.948236190979496 4.193185165257813, 2.9357121069393677 4.189386025899021, 2.923463313526982 4.184775906502257, 2.9115422619561997 4.179374548306537, 2.9000000000000004 4.173205080756888, 2.8888859533960796 4.166293922460509, 2.878247714198256 4.158670668058247, 2.8681308369799865 4.150367961495795, 2.8585786437626908 4.141421356237309, 2.8496320385042044 4.1318691630200135, 2.841329331941753 4.121752285801744, 2.833706077539491 4.11111404660392, 2.8267949192431123 4.1, 2.8206254516934623 4.0884577380438, 2.8152240934977426 4.076536686473018, 2.8106139741009786 4.064287893060632, 2.8068148347421866 4.051763809020504, 2.8038429439193537 4.039018064403225, 2.801711027725238 4.026105238444011, 2.8004282153522793 4.013080625846029, 2.8 4, 2.8004282153522793 3.9869193741539712, 2.801711027725238 3.97389476155599, 2.8038429439193537 3.9609819355967746, 2.8068148347421866 3.948236190979496, 2.810613974100979 3.9357121069393677, 2.8152240934977426 3.923463313526982, 2.8206254516934623 3.9115422619561997, 2.8267949192431123 3.9, 2.833706077539491 3.8888859533960796, 2.841329331941753 3.878247714198256, 2.8496320385042044 3.8681308369799865, 2.8585786437626908 3.8585786437626908, 2.8681308369799865 3.8496320385042044, 2.878247714198256 3.841329331941753, 2.8888859533960796 3.833706077539491, 2.9 3.8267949192431123, 2.9115422619561997 3.8206254516934623, 2.923463313526982 3.8152240934977426, 2.9357121069393677 3.810613974100979, 2.948236190979496 3.806814834742186, 2.9609819355967746 3.8038429439193537, 2.9738947615559894 3.8017110277252377, 2.9869193741539712 3.8004282153522793, 3 3.8)))");

        // empty geometry
        assertThat(assertions.function("ST_Buffer", "ST_GeometryFromText('POINT EMPTY')", "1"))
                .isNull(GEOMETRY);

        // negative distance
        assertTrinoExceptionThrownBy(assertions.function("ST_Buffer", "ST_Point(0, 0)", "-1.2")::evaluate)
                .hasMessage("distance is negative");

        assertTrinoExceptionThrownBy(assertions.function("ST_Buffer", "ST_Point(0, 0)", "-infinity()")::evaluate)
                .hasMessage("distance is negative");

        // infinity() and nan() distance
        assertThat(assertions.function("ST_AsText", "ST_Buffer(ST_Point(0, 0), infinity())"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOLYGON EMPTY");

        assertTrinoExceptionThrownBy(assertions.function("ST_Buffer", "ST_Point(0, 0)", "nan()")::evaluate)
                .hasMessage("distance is NaN");

        // For small polygons, there was a bug in ESRI that throw an NPE.  This
        // was fixed (https://github.com/Esri/geometry-api-java/pull/243) to
        // return an empty geometry instead. Ideally, these would return
        // something approximately like `ST_Buffer(ST_Centroid(geometry))`.
        assertThat(assertions.function("ST_IsEmpty", "ST_Buffer(ST_Buffer(ST_Point(177.50102959662, 64.726807421691), 0.0000000001), 0.00005)"))
                .hasType(BOOLEAN)
                .isEqualTo(true);

        assertThat(assertions.function("ST_IsEmpty", "ST_Buffer(ST_GeometryFromText(" +
                "'POLYGON ((177.0 64.0, 177.0000000001 64.0, 177.0000000001 64.0000000001, 177.0 64.0000000001, 177.0 64.0))'), 0.01)"))
                .hasType(BOOLEAN)
                .isEqualTo(true);
    }

    @Test
    public void testSTCentroid()
    {
        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('LINESTRING EMPTY'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('POINT (3 5)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (3 5)");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('MULTIPOINT (1 2, 2 4, 3 6, 4 8)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (2.5 5)");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('LINESTRING (1 1, 2 2, 3 3)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (2 2)");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (3 2)");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (2.5 2.5)");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('POLYGON ((1 1, 5 1, 3 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (3 2)");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (3.3333333333333335 4)");

        assertThat(assertions.function("ST_AsText", "ST_Centroid(ST_GeometryFromText('POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (2.5416666666666665 2.5416666666666665)");

        assertApproximateCentroid("MULTIPOLYGON (((4.903234300000006 52.08474289999999, 4.903234265193165 52.084742934806826, 4.903234299999999 52.08474289999999, 4.903234300000006 52.08474289999999)))", new Point(4.9032343, 52.0847429), 1e-7);

        // Numerical stability tests
        assertApproximateCentroid(
                "MULTIPOLYGON (((153.492818 -28.13729, 153.492821 -28.137291, 153.492816 -28.137289, 153.492818 -28.13729)))",
                new Point(153.49282, -28.13729), 1e-5);
        assertApproximateCentroid(
                "MULTIPOLYGON (((153.112475 -28.360526, 153.1124759 -28.360527, 153.1124759 -28.360526, 153.112475 -28.360526)))",
                new Point(153.112475, -28.360526), 1e-5);
        assertApproximateCentroid(
                "POLYGON ((4.903234300000006 52.08474289999999, 4.903234265193165 52.084742934806826, 4.903234299999999 52.08474289999999, 4.903234300000006 52.08474289999999))",
                new Point(4.9032343, 52.0847429), 1e-6);
        assertApproximateCentroid(
                "MULTIPOLYGON (((4.903234300000006 52.08474289999999, 4.903234265193165 52.084742934806826, 4.903234299999999 52.08474289999999, 4.903234300000006 52.08474289999999)))",
                new Point(4.9032343, 52.0847429), 1e-6);
        assertApproximateCentroid(
                "POLYGON ((-81.0387349 29.20822, -81.039974 29.210597, -81.0410331 29.2101579, -81.0404758 29.2090879, -81.0404618 29.2090609, -81.040433 29.209005, -81.0404269 29.208993, -81.0404161 29.2089729, -81.0398001 29.20779, -81.0387349 29.20822), (-81.0404229 29.208986, -81.04042 29.2089809, -81.0404269 29.208993, -81.0404229 29.208986))",
                new Point(-81.039885, 29.209191), 1e-6);
    }

    private void assertApproximateCentroid(String wkt, Point expectedCentroid, double epsilon)
    {
        OGCPoint actualCentroid = (OGCPoint) GeometrySerde.deserialize(
                stCentroid(GeometrySerde.serialize(OGCGeometry.fromText(wkt))));
        assertThat(expectedCentroid.getX()).isCloseTo(actualCentroid.X(), within(epsilon));
        assertThat(expectedCentroid.getY()).isCloseTo(actualCentroid.Y(), within(epsilon));
    }

    @Test
    public void testSTConvexHull()
    {
        // test empty geometry
        assertConvexHull("POINT EMPTY", "POINT EMPTY");
        assertConvexHull("MULTIPOINT EMPTY", "MULTIPOINT EMPTY");
        assertConvexHull("LINESTRING EMPTY", "LINESTRING EMPTY");
        assertConvexHull("MULTILINESTRING EMPTY", "MULTILINESTRING EMPTY");
        assertConvexHull("POLYGON EMPTY", "POLYGON EMPTY");
        assertConvexHull("MULTIPOLYGON EMPTY", "MULTIPOLYGON EMPTY");
        assertConvexHull("GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION EMPTY");
        assertConvexHull("GEOMETRYCOLLECTION (POINT (1 1), POINT EMPTY)", "POINT (1 1)");
        assertConvexHull("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (1 1), GEOMETRYCOLLECTION (POINT (1 5), POINT (4 5), GEOMETRYCOLLECTION (POINT (3 4), POINT EMPTY))))", "POLYGON ((1 1, 4 5, 1 5, 1 1))");

        // test single geometry
        assertConvexHull("POINT (1 1)", "POINT (1 1)");
        assertConvexHull("LINESTRING (1 1, 1 9, 2 2)", "POLYGON ((1 1, 2 2, 1 9, 1 1))");

        // convex single geometry
        assertConvexHull("LINESTRING (1 1, 1 9, 2 2, 1 1)", "POLYGON ((1 1, 2 2, 1 9, 1 1))");
        assertConvexHull("POLYGON ((0 0, 0 3, 2 4, 4 2, 3 0, 0 0))", "POLYGON ((0 0, 3 0, 4 2, 2 4, 0 3, 0 0))");

        // non-convex geometry
        assertConvexHull("LINESTRING (1 1, 1 9, 2 2, 1 1, 4 0)", "POLYGON ((1 1, 4 0, 1 9, 1 1))");
        assertConvexHull("POLYGON ((0 0, 0 3, 4 4, 1 1, 3 0))", "POLYGON ((0 0, 3 0, 4 4, 0 3, 0 0))");

        // all points are on the same line
        assertConvexHull("LINESTRING (20 20, 30 30)", "LINESTRING (20 20, 30 30)");
        assertConvexHull("MULTILINESTRING ((0 0, 3 3), (1 1, 2 2), (2 2, 4 4), (5 5, 8 8))", "LINESTRING (0 0, 8 8)");
        assertConvexHull("MULTIPOINT (0 1, 1 2, 2 3, 3 4, 4 5, 5 6)", "LINESTRING (0 1, 5 6)");
        assertConvexHull("GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (1 1, 4 4, 2 2), POINT (10 10), POLYGON ((5 5, 7 7)), POINT (2 2), LINESTRING (6 6, 9 9), POLYGON ((1 1)))", "LINESTRING (0 0, 10 10)");
        assertConvexHull("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (2 2), POINT (1 1)), POINT (3 3))", "LINESTRING (3 3, 1 1)");

        // not all points are on the same line
        assertConvexHull("MULTILINESTRING ((1 1, 5 1, 6 6), (2 4, 4 0), (2 -4, 4 4), (3 -2, 4 -3))", "POLYGON ((1 1, 2 -4, 4 -3, 5 1, 6 6, 2 4, 1 1))");
        assertConvexHull("MULTIPOINT (0 2, 1 0, 3 0, 4 0, 4 2, 2 2, 2 4)", "POLYGON ((0 2, 1 0, 4 0, 4 2, 2 4, 0 2))");
        assertConvexHull("MULTIPOLYGON (((0 3, 2 0, 3 6), (2 1, 2 3, 5 3, 5 1), (1 7, 2 4, 4 2, 5 6, 3 8)))", "POLYGON ((0 3, 2 0, 5 1, 5 6, 3 8, 1 7, 0 3))");
        assertConvexHull("GEOMETRYCOLLECTION (POINT (2 3), LINESTRING (2 8, 7 10), POINT (8 10), POLYGON ((4 4, 4 8, 9 8, 6 6, 6 4, 8 3, 6 1)), POINT (4 2), LINESTRING (3 6, 5 5), POLYGON ((7 5, 7 6, 8 6, 8 5)))", "POLYGON ((2 3, 6 1, 8 3, 9 8, 8 10, 7 10, 2 8, 2 3))");
        assertConvexHull("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (2 3), LINESTRING (2 8, 7 10), GEOMETRYCOLLECTION (POINT (8 10))), POLYGON ((4 4, 4 8, 9 8, 6 6, 6 4, 8 3, 6 1)), POINT (4 2), LINESTRING (3 6, 5 5), POLYGON ((7 5, 7 6, 8 6, 8 5)))", "POLYGON ((2 3, 6 1, 8 3, 9 8, 8 10, 7 10, 2 8, 2 3))");

        // single-element multi-geometries and geometry collections
        assertConvexHull("MULTILINESTRING ((1 1, 5 1, 6 6))", "POLYGON ((1 1, 5 1, 6 6, 1 1))");
        assertConvexHull("MULTILINESTRING ((1 1, 5 1, 1 4, 5 4))", "POLYGON ((1 1, 5 1, 5 4, 1 4, 1 1))");
        assertConvexHull("MULTIPOINT (0 2)", "POINT (0 2)");
        assertConvexHull("MULTIPOLYGON (((0 3, 2 0, 3 6)))", "POLYGON ((0 3, 2 0, 3 6, 0 3))");
        assertConvexHull("MULTIPOLYGON (((0 0, 4 0, 4 4, 0 4, 2 2)))", "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))");
        assertConvexHull("GEOMETRYCOLLECTION (POINT (2 3))", "POINT (2 3)");
        assertConvexHull("GEOMETRYCOLLECTION (LINESTRING (1 1, 5 1, 6 6))", "POLYGON ((1 1, 5 1, 6 6, 1 1))");
        assertConvexHull("GEOMETRYCOLLECTION (LINESTRING (1 1, 5 1, 1 4, 5 4))", "POLYGON ((1 1, 5 1, 5 4, 1 4, 1 1))");
        assertConvexHull("GEOMETRYCOLLECTION (POLYGON ((0 3, 2 0, 3 6)))", "POLYGON ((0 3, 2 0, 3 6, 0 3))");
        assertConvexHull("GEOMETRYCOLLECTION (POLYGON ((0 0, 4 0, 4 4, 0 4, 2 2)))", "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))");
    }

    private void assertConvexHull(String inputWKT, String expectWKT)
    {
        assertThat(assertions.expression("ST_AsText(ST_ConvexHull(geometry))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(inputWKT)))
                .hasType(VARCHAR)
                .isEqualTo(expectWKT);
    }

    @Test
    public void testSTCoordDim()
    {
        assertThat(assertions.function("ST_CoordDim", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')"))
                .isEqualTo((byte) 2);

        assertThat(assertions.function("ST_CoordDim", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isEqualTo((byte) 2);

        assertThat(assertions.function("ST_CoordDim", "ST_GeometryFromText('LINESTRING EMPTY')"))
                .isEqualTo((byte) 2);

        assertThat(assertions.function("ST_CoordDim", "ST_GeometryFromText('POINT (1 4)')"))
                .isEqualTo((byte) 2);
    }

    @Test
    public void testSTDimension()
    {
        assertThat(assertions.function("ST_Dimension", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isEqualTo((byte) 2);

        assertThat(assertions.function("ST_Dimension", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')"))
                .isEqualTo((byte) 2);

        assertThat(assertions.function("ST_Dimension", "ST_GeometryFromText('LINESTRING EMPTY')"))
                .isEqualTo((byte) 1);

        assertThat(assertions.function("ST_Dimension", "ST_GeometryFromText('POINT (1 4)')"))
                .isEqualTo((byte) 0);
    }

    @Test
    public void testSTIsClosed()
    {
        assertThat(assertions.function("ST_IsClosed", "ST_GeometryFromText('LINESTRING (1 1, 2 2, 1 3, 1 1)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_IsClosed", "ST_GeometryFromText('LINESTRING (1 1, 2 2, 1 3)')"))
                .isEqualTo(false);

        assertTrinoExceptionThrownBy(assertions.function("ST_IsClosed", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')")::evaluate)
                .hasMessage("ST_IsClosed only applies to LINE_STRING or MULTI_LINE_STRING. Input type is: POLYGON");
    }

    @Test
    public void testSTIsEmpty()
    {
        assertThat(assertions.function("ST_IsEmpty", "ST_GeometryFromText('POINT (1.5 2.5)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_IsEmpty", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isEqualTo(true);
    }

    private void assertSimpleGeometry(String text)
    {
        assertThat(assertions.function("ST_IsSimple", "ST_GeometryFromText('%s')".formatted(text)))
                .isEqualTo(true);
    }

    private void assertNotSimpleGeometry(String text)
    {
        assertThat(assertions.function("ST_IsSimple", "ST_GeometryFromText('%s')".formatted(text)))
                .isEqualTo(false);
    }

    @Test
    public void testSTIsSimple()
    {
        assertSimpleGeometry("POINT (1.5 2.5)");
        assertSimpleGeometry("MULTIPOINT (1 2, 2 4, 3 6, 4 8)");
        assertNotSimpleGeometry("MULTIPOINT (1 2, 2 4, 3 6, 1 2)");
        assertSimpleGeometry("LINESTRING (8 4, 5 7)");
        assertSimpleGeometry("LINESTRING (1 1, 2 2, 1 3, 1 1)");
        assertNotSimpleGeometry("LINESTRING (0 0, 1 1, 1 0, 0 1)");
        assertSimpleGeometry("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))");
        assertNotSimpleGeometry("MULTILINESTRING ((1 1, 5 1), (2 4, 4 0))");
        assertSimpleGeometry("POLYGON EMPTY");
        assertSimpleGeometry("POLYGON ((2 0, 2 1, 3 1))");
        assertSimpleGeometry("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))");
    }

    @Test
    public void testSimplifyGeometry()
    {
        // Eliminate unnecessary points on the same line.
        assertThat(assertions.function("ST_AsText", "simplify_geometry(ST_GeometryFromText('POLYGON ((1 0, 2 1, 3 1, 3 1, 4 1, 1 0))'), 1.5)"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 0, 4 1, 2 1, 1 0))");

        // Use distanceTolerance to control fidelity.
        assertThat(assertions.function("ST_AsText", "simplify_geometry(ST_GeometryFromText('POLYGON ((1 0, 1 1, 2 1, 2 3, 3 3, 3 1, 4 1, 4 0, 1 0))'), 1.0)"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 0, 4 0, 3 3, 2 3, 1 0))");

        assertThat(assertions.function("ST_AsText", "simplify_geometry(ST_GeometryFromText('POLYGON ((1 0, 1 1, 2 1, 2 3, 3 3, 3 1, 4 1, 4 0, 1 0))'), 0.5)"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 0, 4 0, 4 1, 3 1, 3 3, 2 3, 2 1, 1 1, 1 0))");

        // Negative distance tolerance is invalid.
        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "simplify_geometry(ST_GeometryFromText('POLYGON ((1 0, 1 1, 2 1, 2 3, 3 3, 3 1, 4 1, 4 0, 1 0))'), -0.5)")::evaluate)
                .hasMessage("distanceTolerance is negative");
    }

    @Test
    public void testSTIsValid()
    {
        // empty geometries are valid
        assertValidGeometry("POINT EMPTY");
        assertValidGeometry("MULTIPOINT EMPTY");
        assertValidGeometry("LINESTRING EMPTY");
        assertValidGeometry("MULTILINESTRING EMPTY");
        assertValidGeometry("POLYGON EMPTY");
        assertValidGeometry("MULTIPOLYGON EMPTY");
        assertValidGeometry("GEOMETRYCOLLECTION EMPTY");

        // valid geometries
        assertValidGeometry("POINT (1 2)");
        assertValidGeometry("MULTIPOINT (1 2, 3 4)");
        assertValidGeometry("LINESTRING (0 0, 1 2, 3 4)");
        assertValidGeometry("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))");
        assertValidGeometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))");
        assertValidGeometry("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))");
        assertValidGeometry("GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (0 0, 1 2, 3 4), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))");

        // invalid geometries
        assertInvalidGeometry("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))", "Repeated points at or near (0.0 1.0) and (0.0 1.0)");
        assertInvalidGeometry("LINESTRING (0 0, 0 1, 0 1, 1 1, 1 0, 0 0)", "Degenerate segments at or near (0.0 1.0)");
        assertInvalidGeometry("LINESTRING (0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0)", "Self-tangency at or near (0.0 1.0) and (0.0 1.0)");
        assertInvalidGeometry("POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))", "Intersecting or overlapping segments at or near (1.0 0.0) and (1.0 1.0)");
        assertInvalidGeometry("POLYGON ((0 0, 0 1, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))", "Degenerate segments at or near (0.0 1.0)");
        assertInvalidGeometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))", "RingOrientation");
        assertInvalidGeometry("POLYGON ((0 0, 0 1, 2 1, 1 1, 1 0, 0 0))", "Intersecting or overlapping segments at or near (0.0 1.0) and (2.0 1.0)");
        assertInvalidGeometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (0 1, 1 1, 0.5 0.5, 0 1))", "Self-intersection at or near (0.0 1.0) and (1.0 1.0)");
        assertInvalidGeometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (0 0, 0.5 0.7, 1 1, 0.5 0.4, 0 0))", "Disconnected interior at or near (0.0 1.0)");
        assertInvalidGeometry("POLYGON ((0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0))", "Self-tangency at or near (0.0 1.0) and (0.0 1.0)");
        assertInvalidGeometry("MULTIPOLYGON (((0 0, 0 1, 1 1, 1 0, 0 0)), ((0.5 0.5, 0.5 2, 2 2, 2 0.5, 0.5 0.5)))", "Intersecting or overlapping segments at or near (0.0 1.0) and (0.5 0.5)");
        assertInvalidGeometry("GEOMETRYCOLLECTION (POINT (1 2), POLYGON ((0 0, 0 1, 2 1, 1 1, 1 0, 0 0)))", "Intersecting or overlapping segments at or near (0.0 1.0) and (2.0 1.0)");

        // corner cases
        assertThat(assertions.function("ST_IsValid", "ST_GeometryFromText(null)"))
                .isNull(BOOLEAN);

        assertThat(assertions.function("geometry_invalid_reason", "ST_GeometryFromText(null)"))
                .isNull(VARCHAR);
    }

    private void assertValidGeometry(String wkt)
    {
        assertThat(assertions.function("ST_IsValid", "ST_GeometryFromText('%s')".formatted(wkt)))
                .isEqualTo(true);

        assertThat(assertions.function("geometry_invalid_reason", "ST_GeometryFromText('%s')".formatted(wkt)))
                .isNull(VARCHAR);
    }

    private void assertInvalidGeometry(String wkt, String reason)
    {
        assertThat(assertions.function("ST_IsValid", "ST_GeometryFromText('%s')".formatted(wkt)))
                .isEqualTo(false);

        assertThat(assertions.function("geometry_invalid_reason", "ST_GeometryFromText('%s')".formatted(wkt)))
                .hasType(VARCHAR)
                .isEqualTo(reason);
    }

    @Test
    public void testSTLength()
    {
        assertThat(assertions.function("ST_Length", "ST_GeometryFromText('LINESTRING EMPTY')"))
                .isEqualTo(0.0);

        assertThat(assertions.function("ST_Length", "ST_GeometryFromText('LINESTRING (0 0, 2 2)')"))
                .isEqualTo(2.8284271247461903);

        assertThat(assertions.function("ST_Length", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')"))
                .isEqualTo(6.0);

        assertTrinoExceptionThrownBy(assertions.function("ST_Length", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')")::evaluate)
                .hasMessage("ST_Length only applies to LINE_STRING or MULTI_LINE_STRING. Input type is: POLYGON");
    }

    @Test
    public void testSTLengthSphericalGeography()
    {
        // Empty linestring returns null
        assertSTLengthSphericalGeography("LINESTRING EMPTY", null);

        // Linestring with one point has length 0
        assertSTLengthSphericalGeography("LINESTRING (0 0)", 0.0);

        // Linestring with only one distinct point has length 0
        assertSTLengthSphericalGeography("LINESTRING (0 0, 0 0, 0 0)", 0.0);

        double length = 4350866.6362;

        // ST_Length is equivalent to sums of ST_DISTANCE between points in the LineString
        assertSTLengthSphericalGeography("LINESTRING (-71.05 42.36, -87.62 41.87, -122.41 37.77)", length);

        // Linestring has same length as its reverse
        assertSTLengthSphericalGeography("LINESTRING (-122.41 37.77, -87.62 41.87, -71.05 42.36)", length);

        // Path north pole -> south pole -> north pole should be roughly the circumference of the Earth
        assertSTLengthSphericalGeography("LINESTRING (0.0 90.0, 0.0 -90.0, 0.0 90.0)", 4.003e7);

        // Empty multi-linestring returns null
        assertSTLengthSphericalGeography("MULTILINESTRING (EMPTY)", null);

        // Multi-linestring with one path is equivalent to a single linestring
        assertSTLengthSphericalGeography("MULTILINESTRING ((-71.05 42.36, -87.62 41.87, -122.41 37.77))", length);

        // Multi-linestring with two disjoint paths has length equal to sum of lengths of lines
        assertSTLengthSphericalGeography("MULTILINESTRING ((-71.05 42.36, -87.62 41.87, -122.41 37.77), (-73.05 42.36, -89.62 41.87, -124.41 37.77))", 2 * length);

        // Multi-linestring with adjacent paths is equivalent to a single linestring
        assertSTLengthSphericalGeography("MULTILINESTRING ((-71.05 42.36, -87.62 41.87), (-87.62 41.87, -122.41 37.77))", length);
    }

    private void assertSTLengthSphericalGeography(String lineString, Double expectedLength)
    {
        if (expectedLength == null || expectedLength == 0.0) {
            assertThat(assertions.function("ST_Length", "to_spherical_geography(ST_GeometryFromText('%s'))".formatted(lineString)))
                    .isEqualTo(expectedLength);
        }
        else {
            assertThat(assertions.expression("ROUND(ABS((ST_Length(to_spherical_geography(ST_GeometryFromText('%s'))) / %f) - 1.0) / %f, 0)".formatted(lineString, expectedLength, 1e-4)))
                    .isEqualTo(0.0);
        }
    }

    @Test
    public void testLineLocatePoint()
    {
        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_Point(0, 0.2)"))
                .isEqualTo(0.2);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_Point(0, 0)"))
                .isEqualTo(0.0);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_Point(0, -1)"))
                .isEqualTo(0.0);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_Point(0, 1)"))
                .isEqualTo(1.0);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_Point(0, 2)"))
                .isEqualTo(1.0);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1, 2 1)')", "ST_Point(0, 0.2)"))
                .isEqualTo(0.06666666666666667);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1, 2 1)')", "ST_Point(0.9, 1)"))
                .isEqualTo(0.6333333333333333);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (1 3, 5 4)')", "ST_Point(1, 3)"))
                .isEqualTo(0.0);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (1 3, 5 4)')", "ST_Point(2, 3)"))
                .isEqualTo(0.23529411764705882);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (1 3, 5 4)')", "ST_Point(5, 4)"))
                .isEqualTo(1.0);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('MULTILINESTRING ((0 0, 0 1), (2 2, 4 2))')", "ST_Point(3, 1)"))
                .isEqualTo(0.6666666666666666);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING EMPTY')", "ST_Point(0, 1)"))
                .isNull(DOUBLE);

        assertThat(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1, 2 1)')", "ST_GeometryFromText('POINT EMPTY')"))
                .isNull(DOUBLE);

        assertTrinoExceptionThrownBy(assertions.function("line_locate_point", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')", "ST_Point(0.4, 1)")::evaluate)
                .hasMessage("First argument to line_locate_point must be a LineString or a MultiLineString. Got: Polygon");

        assertTrinoExceptionThrownBy(assertions.function("line_locate_point", "ST_GeometryFromText('LINESTRING (0 0, 0 1, 2 1)')", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')")::evaluate)
                .hasMessage("Second argument to line_locate_point must be a Point. Got: Polygon");
    }

    @Test
    public void testLineInterpolatePoint()
    {
        assertThat(assertions.function("ST_AsText", "line_interpolate_point(ST_GeometryFromText('LINESTRING EMPTY'), 0.5)"))
                .isNull(VARCHAR);

        assertLineInterpolatePoint("LINESTRING (0 0, 1 1, 10 10)", 0.0, "POINT (0 0)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1, 10 10)", 0.1, "POINT (1 1)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1, 10 10)", 0.05, "POINT (0.5 0.5)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1, 10 10)", 0.4, "POINT (4.000000000000001 4.000000000000001)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1, 10 10)", 1.0, "POINT (10 10)");

        assertLineInterpolatePoint("LINESTRING (0 0, 1 1)", 0.0, "POINT (0 0)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1)", 0.1, "POINT (0.1 0.1)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1)", 0.05, "POINT (0.05 0.05)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1)", 0.4, "POINT (0.4 0.4)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 1)", 1.0, "POINT (1 1)");

        assertLineInterpolatePoint("LINESTRING (0 0, 1 0, 1 9)", 0.0, "POINT (0 0)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 0, 1 9)", 0.05, "POINT (0.5 0)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 0, 1 9)", 0.1, "POINT (1 0)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 0, 1 9)", 0.5, "POINT (1 4)");
        assertLineInterpolatePoint("LINESTRING (0 0, 1 0, 1 9)", 1.0, "POINT (1 9)");

        assertTrinoExceptionThrownBy(assertions.function("line_interpolate_point", "ST_GeometryFromText('LINESTRING (0 0, 1 0, 1 9)')", "-0.5")::evaluate)
                .hasMessage("fraction must be between 0 and 1");

        assertTrinoExceptionThrownBy(assertions.function("line_interpolate_point", "ST_GeometryFromText('LINESTRING (0 0, 1 0, 1 9)')", "2.0")::evaluate)
                .hasMessage("fraction must be between 0 and 1");

        assertTrinoExceptionThrownBy(assertions.function("line_interpolate_point", "ST_GeometryFromText('POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))')", "0.2")::evaluate)
                .hasMessage("line_interpolate_point only applies to LINE_STRING. Input type is: POLYGON");
    }

    @Test
    public void testLineInterpolatePoints()
    {
        assertThat(assertions.function("line_interpolate_points", "ST_GeometryFromText('LINESTRING EMPTY')", "0.5"))
                .isNull(new ArrayType(GEOMETRY));

        assertLineInterpolatePoints("LINESTRING (0 0, 1 1, 10 10)", 0.0, "0 0");
        assertLineInterpolatePoints("LINESTRING (0 0, 1 1, 10 10)", 0.4, "4.000000000000001 4.000000000000001", "8 8");
        assertLineInterpolatePoints("LINESTRING (0 0, 1 1, 10 10)", 0.3, "3 3", "6 6", "9 9");
        assertLineInterpolatePoints("LINESTRING (0 0, 1 1, 10 10)", 0.5, "5.000000000000001 5.000000000000001", "10 10");
        assertLineInterpolatePoints("LINESTRING (0 0, 1 1, 10 10)", 1, "10 10");

        assertTrinoExceptionThrownBy(assertions.function("line_interpolate_points", "ST_GeometryFromText('LINESTRING (0 0, 1 0, 1 9)')", "-0.5")::evaluate)
                .hasMessage("fraction must be between 0 and 1");

        assertTrinoExceptionThrownBy(assertions.function("line_interpolate_points", "ST_GeometryFromText('LINESTRING (0 0, 1 0, 1 9)')", "2.0")::evaluate)
                .hasMessage("fraction must be between 0 and 1");

        assertTrinoExceptionThrownBy(assertions.function("line_interpolate_points", "ST_GeometryFromText('POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))')", "0.2")::evaluate)
                .hasMessage("line_interpolate_point only applies to LINE_STRING. Input type is: POLYGON");
    }

    private void assertLineInterpolatePoint(String wkt, double fraction, String expectedPoint)
    {
        assertThat(assertions.expression("ST_AsText(line_interpolate_point(geometry, fraction))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                .binding("fraction", Double.toString(fraction)))
                .hasType(VARCHAR)
                .isEqualTo(expectedPoint);
    }

    private void assertLineInterpolatePoints(String wkt, double fraction, String... expected)
    {
        assertThat(assertions.expression("transform(line_interpolate_points(geometry, fraction), x -> ST_AsText(x))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                .binding("fraction", Double.toString(fraction)))
                .hasType(new ArrayType(VARCHAR))
                .isEqualTo(Arrays.stream(expected).map(s -> "POINT (" + s + ")").collect(toImmutableList()));
    }

    @Test
    public void testSTMax()
    {
        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('POINT (1.5 2.5)')"))
                .isEqualTo(1.5);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('POINT (1.5 2.5)')"))
                .isEqualTo(2.5);

        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('MULTIPOINT (1 2, 2 4, 3 6, 4 8)')"))
                .isEqualTo(4.0);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('MULTIPOINT (1 2, 2 4, 3 6, 4 8)')"))
                .isEqualTo(8.0);

        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('LINESTRING (8 4, 5 7)')"))
                .isEqualTo(8.0);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('LINESTRING (8 4, 5 7)')"))
                .isEqualTo(7.0);

        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')"))
                .isEqualTo(5.0);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')"))
                .isEqualTo(4.0);

        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')"))
                .isEqualTo(3.0);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')"))
                .isEqualTo(1.0);

        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))')"))
                .isEqualTo(6.0);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 10, 6 4)))')"))
                .isEqualTo(10.0);

        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_XMax", "ST_GeometryFromText('GEOMETRYCOLLECTION (POINT (5 1), LINESTRING (3 4, 4 4))')"))
                .isEqualTo(5.0);

        assertThat(assertions.function("ST_YMax", "ST_GeometryFromText('GEOMETRYCOLLECTION (POINT (5 1), LINESTRING (3 4, 4 4))')"))
                .isEqualTo(4.0);

        assertThat(assertions.function("ST_XMax", "null"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_YMax", "null"))
                .isNull(DOUBLE);
    }

    @Test
    public void testSTMin()
    {
        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('POINT (1.5 2.5)')"))
                .isEqualTo(1.5);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('POINT (1.5 2.5)')"))
                .isEqualTo(2.5);

        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('MULTIPOINT (1 2, 2 4, 3 6, 4 8)')"))
                .isEqualTo(1.0);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('MULTIPOINT (1 2, 2 4, 3 6, 4 8)')"))
                .isEqualTo(2.0);

        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('LINESTRING (8 4, 5 7)')"))
                .isEqualTo(5.0);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('LINESTRING (8 4, 5 7)')"))
                .isEqualTo(4.0);

        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')"))
                .isEqualTo(1.0);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('MULTILINESTRING ((1 2, 5 3), (2 4, 4 4))')"))
                .isEqualTo(2.0);

        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')"))
                .isEqualTo(2.0);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')"))
                .isEqualTo(0.0);

        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('MULTIPOLYGON (((1 10, 1 3, 3 3, 3 10)), ((2 4, 2 6, 6 6, 6 4)))')"))
                .isEqualTo(1.0);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('MULTIPOLYGON (((1 10, 1 3, 3 3, 3 10)), ((2 4, 2 6, 6 10, 6 4)))')"))
                .isEqualTo(3.0);

        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_XMin", "ST_GeometryFromText('GEOMETRYCOLLECTION (POINT (5 1), LINESTRING (3 4, 4 4))')"))
                .isEqualTo(3.0);

        assertThat(assertions.function("ST_YMin", "ST_GeometryFromText('GEOMETRYCOLLECTION (POINT (5 1), LINESTRING (3 4, 4 4))')"))
                .isEqualTo(1.0);

        assertThat(assertions.function("ST_XMin", "null"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_YMin", "null"))
                .isNull(DOUBLE);
    }

    @Test
    public void testSTNumInteriorRing()
    {
        assertThat(assertions.function("ST_NumInteriorRing", "ST_GeometryFromText('POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))')"))
                .isEqualTo(0L);

        assertThat(assertions.function("ST_NumInteriorRing", "ST_GeometryFromText('POLYGON ((0 0, 8 0, 0 8, 0 0), (1 1, 1 5, 5 1, 1 1))')"))
                .isEqualTo(1L);

        assertTrinoExceptionThrownBy(assertions.function("ST_NumInteriorRing", "ST_GeometryFromText('LINESTRING (8 4, 5 7)')")::evaluate)
                .hasMessage("ST_NumInteriorRing only applies to POLYGON. Input type is: LINE_STRING");
    }

    @Test
    public void testSTNumPoints()
    {
        assertNumPoints("POINT EMPTY", 0);
        assertNumPoints("MULTIPOINT EMPTY", 0);
        assertNumPoints("LINESTRING EMPTY", 0);
        assertNumPoints("MULTILINESTRING EMPTY", 0);
        assertNumPoints("POLYGON EMPTY", 0);
        assertNumPoints("MULTIPOLYGON EMPTY", 0);
        assertNumPoints("GEOMETRYCOLLECTION EMPTY", 0);

        assertNumPoints("POINT (1 2)", 1);
        assertNumPoints("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", 4);
        assertNumPoints("LINESTRING (8 4, 5 7)", 2);
        assertNumPoints("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", 4);
        assertNumPoints("POLYGON ((0 0, 8 0, 0 8, 0 0), (1 1, 1 5, 5 1, 1 1))", 6);
        assertNumPoints("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))", 8);
        assertNumPoints("GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (8 4, 5 7), POLYGON EMPTY)", 3);
    }

    private void assertNumPoints(String wkt, int expectedPoints)
    {
        assertThat(assertions.function("ST_NumPoints", "ST_GeometryFromText('%s')".formatted(wkt)))
                .isEqualTo((long) expectedPoints);
    }

    @Test
    public void testSTIsRing()
    {
        assertThat(assertions.function("ST_IsRing", "ST_GeometryFromText('LINESTRING (8 4, 4 8)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_IsRing", "ST_GeometryFromText('LINESTRING (0 0, 1 1, 0 2, 0 0)')"))
                .isEqualTo(true);

        assertTrinoExceptionThrownBy(assertions.function("ST_IsRing", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')")::evaluate)
                .hasMessage("ST_IsRing only applies to LINE_STRING. Input type is: POLYGON");
    }

    @Test
    public void testSTStartEndPoint()
    {
        assertThat(assertions.function("ST_AsText", "ST_StartPoint(ST_GeometryFromText('LINESTRING (8 4, 4 8, 5 6)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (8 4)");

        assertThat(assertions.function("ST_AsText", "ST_EndPoint(ST_GeometryFromText('LINESTRING (8 4, 4 8, 5 6)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (5 6)");

        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "ST_StartPoint(ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))'))")::evaluate)
                .hasMessage("ST_StartPoint only applies to LINE_STRING. Input type is: POLYGON");

        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "ST_EndPoint(ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))'))")::evaluate)
                .hasMessage("ST_EndPoint only applies to LINE_STRING. Input type is: POLYGON");
    }

    @Test
    public void testSTPoints()
    {
        assertThat(assertions.function("ST_Points", "ST_GeometryFromText('LINESTRING EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertSTPoints("LINESTRING (0 0, 0 0)", "0 0", "0 0");
        assertSTPoints("LINESTRING (8 4, 3 9, 8 4)", "8 4", "3 9", "8 4");
        assertSTPoints("LINESTRING (8 4, 3 9, 5 6)", "8 4", "3 9", "5 6");
        assertSTPoints("LINESTRING (8 4, 3 9, 5 6, 3 9, 8 4)", "8 4", "3 9", "5 6", "3 9", "8 4");

        assertThat(assertions.function("ST_Points", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertSTPoints("POLYGON ((8 4, 3 9, 5 6, 8 4))", "8 4", "5 6", "3 9", "8 4");
        assertSTPoints("POLYGON ((8 4, 3 9, 5 6, 7 2, 8 4))", "8 4", "7 2", "5 6", "3 9", "8 4");

        assertThat(assertions.function("ST_Points", "ST_GeometryFromText('POINT EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertSTPoints("POINT (0 0)", "0 0");
        assertSTPoints("POINT (0 1)", "0 1");

        assertThat(assertions.function("ST_Points", "ST_GeometryFromText('MULTIPOINT EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertSTPoints("MULTIPOINT (0 0)", "0 0");
        assertSTPoints("MULTIPOINT (0 0, 1 2)", "0 0", "1 2");

        assertThat(assertions.function("ST_Points", "ST_GeometryFromText('MULTILINESTRING EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertSTPoints("MULTILINESTRING ((0 0, 1 1), (2 3, 3 2))", "0 0", "1 1", "2 3", "3 2");
        assertSTPoints("MULTILINESTRING ((0 0, 1 1, 1 2), (2 3, 3 2, 5 4))", "0 0", "1 1", "1 2", "2 3", "3 2", "5 4");
        assertSTPoints("MULTILINESTRING ((0 0, 1 1, 1 2), (1 2, 3 2, 5 4))", "0 0", "1 1", "1 2", "1 2", "3 2", "5 4");

        assertThat(assertions.function("ST_Points", "ST_GeometryFromText('MULTIPOLYGON EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertSTPoints("MULTIPOLYGON (((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1)), ((-1 -1, -1 -2, -2 -2, -2 -1, -1 -1)))",
                "0 0", "0 4", "4 4", "4 0", "0 0",
                "1 1", "2 1", "2 2", "1 2", "1 1",
                "-1 -1", "-1 -2", "-2 -2", "-2 -1", "-1 -1");

        assertThat(assertions.function("ST_Points", "ST_GeometryFromText('GEOMETRYCOLLECTION EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        String newLine = System.getProperty("line.separator");
        String geometryCollection = String.join(newLine,
                "GEOMETRYCOLLECTION(",
                "          POINT ( 0 1 ),",
                "          LINESTRING ( 0 3, 3 4 ),",
                "          POLYGON (( 2 0, 2 3, 0 2, 2 0 )),",
                "          POLYGON (( 3 0, 3 3, 6 3, 6 0, 3 0 ),",
                "                   ( 5 1, 4 2, 5 2, 5 1 )),",
                "          MULTIPOLYGON (",
                "                  (( 0 5, 0 8, 4 8, 4 5, 0 5 ),",
                "                   ( 1 6, 3 6, 2 7, 1 6 )),",
                "                  (( 5 4, 5 8, 6 7, 5 4 ))",
                "           )",
                ")");
        assertSTPoints(geometryCollection, "0 1", "0 3", "3 4", "2 0", "0 2", "2 3", "2 0", "3 0", "3 3", "6 3", "6 0", "3 0",
                "5 1", "5 2", "4 2", "5 1", "0 5", "0 8", "4 8", "4 5", "0 5", "1 6", "3 6", "2 7", "1 6", "5 4", "5 8", "6 7", "5 4");
    }

    private void assertSTPoints(String wkt, String... expected)
    {
        assertThat(assertions.expression("transform(ST_Points(geometry), x -> ST_AsText(x))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt)))
                .hasType(new ArrayType(VARCHAR))
                .isEqualTo(Arrays.stream(expected).map(s -> "POINT (" + s + ")").collect(toImmutableList()));
    }

    @Test
    public void testSTXY()
    {
        assertThat(assertions.function("ST_Y", "ST_GeometryFromText('POINT EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_X", "ST_GeometryFromText('POINT (1 2)')"))
                .isEqualTo(1.0);

        assertThat(assertions.function("ST_Y", "ST_GeometryFromText('POINT (1 2)')"))
                .isEqualTo(2.0);

        assertTrinoExceptionThrownBy(assertions.function("ST_Y", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')")::evaluate)
                .hasMessage("ST_Y only applies to POINT. Input type is: POLYGON");
    }

    @Test
    public void testSTBoundary()
    {
        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('POINT (1 2)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('MULTIPOINT (1 2, 2 4, 3 6, 4 8)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('LINESTRING EMPTY'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('LINESTRING (8 4, 5 7)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT ((8 4), (5 7))");

        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('LINESTRING (100 150,50 60, 70 80, 160 170)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT ((100 150), (160 170))");

        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT ((1 1), (5 1), (2 4), (4 4))");

        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('POLYGON ((1 1, 4 1, 1 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTILINESTRING ((1 1, 4 1, 1 4, 1 1))");

        assertThat(assertions.function("ST_AsText", "ST_Boundary(ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTILINESTRING ((1 1, 3 1, 3 3, 1 3, 1 1), (0 0, 2 0, 2 2, 0 2, 0 0))");
    }

    @Test
    public void testSTEnvelope()
    {
        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('MULTIPOINT (1 2, 2 4, 3 6, 4 8)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 2, 4 2, 4 8, 1 8, 1 2))");

        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('LINESTRING EMPTY'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('LINESTRING (1 1, 2 2, 1 3)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 1, 2 1, 2 3, 1 3, 1 1))");

        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('LINESTRING (8 4, 5 7)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((5 4, 8 4, 8 7, 5 7, 5 4))");

        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 1, 5 1, 5 4, 1 4, 1 1))");

        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('POLYGON ((1 1, 4 1, 1 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 1, 4 1, 4 4, 1 4, 1 1))");

        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0))");

        assertThat(assertions.function("ST_AsText", "ST_Envelope(ST_GeometryFromText('GEOMETRYCOLLECTION (POINT (5 1), LINESTRING (3 4, 4 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((3 1, 5 1, 5 4, 3 4, 3 1))");
    }

    @Test
    public void testSTEnvelopeAsPts()
    {
        assertEnvelopeAsPts("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", new Point(1, 2), new Point(4, 8));
        assertThat(assertions.function("ST_EnvelopeAsPts", "ST_GeometryFromText('LINESTRING EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertEnvelopeAsPts("LINESTRING (1 1, 2 2, 1 3)", new Point(1, 1), new Point(2, 3));
        assertEnvelopeAsPts("LINESTRING (8 4, 5 7)", new Point(5, 4), new Point(8, 7));
        assertEnvelopeAsPts("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", new Point(1, 1), new Point(5, 4));
        assertEnvelopeAsPts("POLYGON ((1 1, 4 1, 1 4))", new Point(1, 1), new Point(4, 4));
        assertEnvelopeAsPts("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))", new Point(0, 0), new Point(3, 3));
        assertEnvelopeAsPts("GEOMETRYCOLLECTION (POINT (5 1), LINESTRING (3 4, 4 4))", new Point(3, 1), new Point(5, 4));
        assertEnvelopeAsPts("POINT (1 2)", new Point(1, 2), new Point(1, 2));
    }

    private void assertEnvelopeAsPts(String wkt, Point lowerLeftCorner, Point upperRightCorner)
    {
        assertThat(assertions.expression("transform(ST_EnvelopeAsPts(geometry), x -> ST_AsText(x))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt)))
                .hasType(new ArrayType(VARCHAR))
                .isEqualTo(ImmutableList.of(new OGCPoint(lowerLeftCorner, null).asText(), new OGCPoint(upperRightCorner, null).asText()));
    }

    @Test
    public void testSTDifference()
    {
        assertThat(assertions.function("ST_AsText", "ST_Difference(ST_GeometryFromText('POINT (50 100)'), ST_GeometryFromText('POINT (150 150)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (50 100)");

        assertThat(assertions.function("ST_AsText", "ST_Difference(ST_GeometryFromText('MULTIPOINT (50 100, 50 200)'), ST_GeometryFromText('POINT (50 100)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (50 200)");

        assertThat(assertions.function("ST_AsText", "ST_Difference(ST_GeometryFromText('LINESTRING (50 100, 50 200)'), ST_GeometryFromText('LINESTRING (50 50, 50 150)'))"))
                .hasType(VARCHAR)
                .isEqualTo("LINESTRING (50 150, 50 200)");

        assertThat(assertions.function("ST_AsText", "ST_Difference(ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))'), ST_GeometryFromText('MULTILINESTRING ((2 1, 4 1), (3 3, 7 3))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTILINESTRING ((1 1, 2 1), (4 1, 5 1), (2 4, 4 4))");

        assertThat(assertions.function("ST_AsText", "ST_Difference(ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))'), ST_GeometryFromText('POLYGON ((2 2, 2 5, 5 5, 5 2))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 1, 4 1, 4 2, 2 2, 2 4, 1 4, 1 1))");

        assertThat(assertions.function("ST_AsText", "ST_Difference(ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))'), ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3, 0 1))'))"))
                .hasType(VARCHAR)
                .isEqualTo("POLYGON ((1 1, 0 1, 0 0, 2 0, 2 1, 1 1))");
    }

    @Test
    public void testSTDistance()
    {
        assertThat(assertions.function("ST_Distance", "ST_Point(50, 100)", "ST_Point(150, 150)"))
                .isEqualTo(111.80339887498948);

        assertThat(assertions.function("ST_Distance", "ST_Point(50, 100)", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(111.80339887498948);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(111.80339887498948);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('MULTIPOINT (50 100, 50 200)')", "ST_GeometryFromText('Point (50 100)')"))
                .isEqualTo(0.0);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('LINESTRING (50 100, 50 200)')", "ST_GeometryFromText('LINESTRING (10 10, 20 20)')"))
                .isEqualTo(85.44003745317531);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('LINESTRING (10 20, 20 50)')"))
                .isEqualTo(17.08800749063506);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((4 4, 4 5, 5 5, 5 4))')"))
                .isEqualTo(1.4142135623730951);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))')", "ST_GeometryFromText('POLYGON ((10 100, 30 10))')"))
                .isEqualTo(27.892651361962706);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('POINT EMPTY')", "ST_Point(150, 150)"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_Distance", "ST_Point(50, 100)", "ST_GeometryFromText('POINT EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('POINT EMPTY')", "ST_GeometryFromText('POINT EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('MULTIPOINT EMPTY')", "ST_GeometryFromText('Point (50 100)')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('LINESTRING (50 100, 50 200)')", "ST_GeometryFromText('LINESTRING EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('MULTILINESTRING EMPTY')", "ST_GeometryFromText('LINESTRING (10 20, 20 50)')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isNull(DOUBLE);

        assertThat(assertions.function("ST_Distance", "ST_GeometryFromText('MULTIPOLYGON EMPTY')", "ST_GeometryFromText('POLYGON ((10 100, 30 10))')"))
                .isNull(DOUBLE);
    }

    @Test
    public void testGeometryNearestPoints()
    {
        assertNearestPoints("POINT (50 100)", "POINT (150 150)", "POINT (50 100)", "POINT (150 150)");
        assertNearestPoints("MULTIPOINT (50 100, 50 200)", "POINT (50 100)", "POINT (50 100)", "POINT (50 100)");
        assertNearestPoints("LINESTRING (50 100, 50 200)", "LINESTRING (10 10, 20 20)", "POINT (50 100)", "POINT (20 20)");
        assertNearestPoints("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", "LINESTRING (10 20, 20 50)", "POINT (4 4)", "POINT (10 20)");
        assertNearestPoints("POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", "POLYGON ((4 4, 4 5, 5 5, 5 4, 4 4))", "POINT (3 3)", "POINT (4 4)");
        assertNearestPoints("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1, 1 1)), ((0 0, 0 2, 2 2, 2 0, 0 0)))", "POLYGON ((10 100, 30 10, 30 100, 10 100))", "POINT (3 3)", "POINT (30 10)");
        assertNearestPoints("GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 20, 20 0))", "POLYGON ((5 5, 5 6, 6 6, 6 5, 5 5))", "POINT (10 10)", "POINT (6 6)");

        assertNoNearestPoints("POINT EMPTY", "POINT (150 150)");
        assertNoNearestPoints("POINT (50 100)", "POINT EMPTY");
        assertNoNearestPoints("POINT EMPTY", "POINT EMPTY");
        assertNoNearestPoints("MULTIPOINT EMPTY", "POINT (50 100)");
        assertNoNearestPoints("LINESTRING (50 100, 50 200)", "LINESTRING EMPTY");
        assertNoNearestPoints("MULTILINESTRING EMPTY", "LINESTRING (10 20, 20 50)");
        assertNoNearestPoints("POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", "POLYGON EMPTY");
        assertNoNearestPoints("MULTIPOLYGON EMPTY", "POLYGON ((10 100, 30 10, 30 100, 10 100))");
    }

    private void assertNearestPoints(String leftInputWkt, String rightInputWkt, String leftPointWkt, String rightPointWkt)
    {
        assertThat(assertions.function("geometry_nearest_points", "ST_GeometryFromText('%s')".formatted(leftInputWkt), "ST_GeometryFromText('%s')".formatted(rightInputWkt)))
                .hasType(RowType.anonymous(ImmutableList.of(GEOMETRY, GEOMETRY)))
                .isEqualTo(ImmutableList.of(leftPointWkt, rightPointWkt));
    }

    private void assertNoNearestPoints(String leftInputWkt, String rightInputWkt)
    {
        assertThat(assertions.function("geometry_nearest_points", "ST_GeometryFromText('%s')".formatted(leftInputWkt), "ST_GeometryFromText('%s')".formatted(rightInputWkt)))
                .isNull(RowType.anonymous(ImmutableList.of(GEOMETRY, GEOMETRY)));
    }

    @Test
    public void testSTExteriorRing()
    {
        assertThat(assertions.function("ST_AsText", "ST_ExteriorRing(ST_GeometryFromText('POLYGON EMPTY'))"))
                .isNull(VARCHAR);

        assertThat(assertions.function("ST_AsText", "ST_ExteriorRing(ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 1))'))"))
                .hasType(VARCHAR)
                .isEqualTo("LINESTRING (1 1, 4 1, 1 4, 1 1)");

        assertThat(assertions.function("ST_AsText", "ST_ExteriorRing(ST_GeometryFromText('POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))'))"))
                .hasType(VARCHAR)
                .isEqualTo("LINESTRING (0 0, 5 0, 5 5, 0 5, 0 0)");

        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "ST_ExteriorRing(ST_GeometryFromText('LINESTRING (1 1, 2 2, 1 3)'))")::evaluate)
                .hasMessage("ST_ExteriorRing only applies to POLYGON. Input type is: LINE_STRING");

        assertTrinoExceptionThrownBy(assertions.function("ST_AsText", "ST_ExteriorRing(ST_GeometryFromText('MULTIPOLYGON (((1 1, 2 2, 1 3, 1 1)), ((4 4, 5 5, 4 6, 4 4)))'))")::evaluate)
                .hasMessage("ST_ExteriorRing only applies to POLYGON. Input type is: MULTI_POLYGON");
    }

    @Test
    public void testSTIntersection()
    {
        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('POINT (50 100)'), ST_GeometryFromText('POINT (150 150)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOLYGON EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('MULTIPOINT (50 100, 50 200)'), ST_GeometryFromText('Point (50 100)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (50 100)");

        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('LINESTRING (50 100, 50 200)'), ST_GeometryFromText('LINESTRING (20 150, 100 150)'))"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (50 150)");

        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))'), ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("GEOMETRYCOLLECTION (POINT (5 1), LINESTRING (3 4, 4 4))");

        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))'), ST_GeometryFromText('POLYGON ((4 4, 4 5, 5 5, 5 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOLYGON EMPTY");

        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))'), ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3))'))"))
                .hasType(VARCHAR)
                .isEqualTo("GEOMETRYCOLLECTION (LINESTRING (1 1, 2 1), MULTIPOLYGON (((0 1, 1 1, 1 2, 0 2, 0 1)), ((2 1, 3 1, 3 3, 1 3, 1 2, 2 2, 2 1))))");

        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))'), ST_GeometryFromText('LINESTRING (2 0, 2 3)'))"))
                .hasType(VARCHAR)
                .isEqualTo("LINESTRING (2 1, 2 3)");

        assertThat(assertions.function("ST_AsText", "ST_Intersection(ST_GeometryFromText('POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))'), ST_GeometryFromText('LINESTRING (0 0, 1 -1, 1 2)'))"))
                .hasType(VARCHAR)
                .isEqualTo("GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (1 0, 1 1))");

        // test intersection of envelopes
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))");
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((-1 4, 1 4, 1 6, -1 6, -1 4))", "POLYGON ((0 4, 1 4, 1 5, 0 5, 0 4))");
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((1 4, 2 4, 2 6, 1 6, 1 4))", "POLYGON ((1 4, 2 4, 2 5, 1 5, 1 4))");
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((4 4, 6 4, 6 6, 4 6, 4 4))", "POLYGON ((4 4, 5 4, 5 5, 4 5, 4 4))");
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((10 10, 11 10, 11 11, 10 11, 10 10))", "POLYGON EMPTY");
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((-1 -1, 0 -1, 0 1, -1 1, -1 -1))", "LINESTRING (0 0, 0 1)");
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((1 -1, 2 -1, 2 0, 1 0, 1 -1))", "LINESTRING (1 0, 2 0)");
        assertEnvelopeIntersection("POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))", "POLYGON ((-1 -1, 0 -1, 0 0, -1 0, -1 -1))", "POINT (0 0)");
    }

    private void assertEnvelopeIntersection(String envelope, String otherEnvelope, String intersection)
    {
        assertThat(assertions.expression("ST_AsText(ST_Intersection(ST_Envelope(a), ST_Envelope(b)))")
                .binding("a", "ST_GeometryFromText('%s')".formatted(envelope))
                .binding("b", "ST_GeometryFromText('%s')".formatted(otherEnvelope)))
                .hasType(VARCHAR)
                .isEqualTo(intersection);
    }

    @Test
    public void testSTSymmetricDifference()
    {
        assertThat(assertions.function("ST_AsText", "ST_SymDifference(ST_GeometryFromText('POINT (50 100)'), ST_GeometryFromText('POINT (50 150)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT ((50 100), (50 150))");

        assertThat(assertions.function("ST_AsText", "ST_SymDifference(ST_GeometryFromText('MULTIPOINT (50 100, 60 200)'), ST_GeometryFromText('MULTIPOINT (60 200, 70 150)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOINT ((50 100), (70 150))");

        assertThat(assertions.function("ST_AsText", "ST_SymDifference(ST_GeometryFromText('LINESTRING (50 100, 50 200)'), ST_GeometryFromText('LINESTRING (50 50, 50 150)'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTILINESTRING ((50 50, 50 100), (50 150, 50 200))");

        assertThat(assertions.function("ST_AsText", "ST_SymDifference(ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))'), ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTILINESTRING ((5 0, 5 1), (1 1, 5 1), (5 1, 5 4), (2 4, 3 4), (4 4, 5 4), (5 4, 6 4))");

        assertThat(assertions.function("ST_AsText", "ST_SymDifference(ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))'), ST_GeometryFromText('POLYGON ((2 2, 2 5, 5 5, 5 2))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOLYGON (((1 1, 4 1, 4 2, 2 2, 2 4, 1 4, 1 1)), ((4 2, 5 2, 5 5, 2 5, 2 4, 4 4, 4 2)))");

        assertThat(assertions.function("ST_AsText", "ST_SymDifference(ST_GeometryFromText('MULTIPOLYGON (((0 0 , 0 2, 2 2, 2 0)), ((2 2, 2 4, 4 4, 4 2)))'), ST_GeometryFromText('POLYGON ((0 0, 0 3, 3 3, 3 0))'))"))
                .hasType(VARCHAR)
                .isEqualTo("MULTIPOLYGON (((2 0, 3 0, 3 2, 2 2, 2 0)), ((0 2, 2 2, 2 3, 0 3, 0 2)), ((3 2, 4 2, 4 4, 2 4, 2 3, 3 3, 3 2)))");
    }

    @Test
    public void testStContains()
    {
        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText(null)", "ST_GeometryFromText('POINT (25 25)')"))
                .isNull(BOOLEAN);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('POINT (20 20)')", "ST_GeometryFromText('POINT (25 25)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('MULTIPOINT (20 20, 25 25)')", "ST_GeometryFromText('POINT (25 25)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('LINESTRING (20 20, 30 30)')", "ST_GeometryFromText('POINT (25 25)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('LINESTRING (20 20, 30 30)')", "ST_GeometryFromText('MULTIPOINT (25 25, 31 31)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('LINESTRING (20 20, 30 30)')", "ST_GeometryFromText('LINESTRING (25 25, 27 27)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('MULTILINESTRING ((3 4, 4 4), (2 1, 6 1))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')", "ST_GeometryFromText('POLYGON ((1 1, 1 2, 2 2, 2 1))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')", "ST_GeometryFromText('POLYGON ((-1 -1, -1 2, 2 2, 2 -1))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('MULTIPOLYGON (((0 0 , 0 2, 2 2, 2 0)), ((2 2, 2 4, 4 4, 4 2)))')", "ST_GeometryFromText('POLYGON ((2 2, 2 3, 3 3, 3 2))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('LINESTRING (20 20, 30 30)')", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('LINESTRING EMPTY')", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Contains", "ST_GeometryFromText('LINESTRING (20 20, 30 30)')", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isEqualTo(false);
    }

    @Test
    public void testSTCrosses()
    {
        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('POINT (20 20)')", "ST_GeometryFromText('POINT (25 25)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('LINESTRING (20 20, 30 30)')", "ST_GeometryFromText('POINT (25 25)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('LINESTRING (20 20, 30 30)')", "ST_GeometryFromText('MULTIPOINT (25 25, 31 31)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('LINESTRING(0 0, 1 1)')", "ST_GeometryFromText('LINESTRING (1 0, 0 1)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')", "ST_GeometryFromText('POLYGON ((2 2, 2 5, 5 5, 5 2))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('MULTIPOLYGON (((0 0 , 0 2, 2 2, 2 0)), ((2 2, 2 4, 4 4, 4 2)))')", "ST_GeometryFromText('POLYGON ((2 2, 2 3, 3 3, 3 2))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('LINESTRING (-2 -2, 6 6)')", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('POINT (20 20)')", "ST_GeometryFromText('POINT (20 20)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Crosses", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')", "ST_GeometryFromText('LINESTRING (0 0, 0 4, 4 4, 4 0)')"))
                .isEqualTo(false);
    }

    @Test
    public void testSTDisjoint()
    {
        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('MULTIPOINT (50 100, 50 200)')", "ST_GeometryFromText('POINT (50 100)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_GeometryFromText('LINESTRING (1 1, 1 0)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('LINESTRING (2 1, 1 2)')", "ST_GeometryFromText('LINESTRING (3 1, 1 3)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('LINESTRING (1 1, 3 3)')", "ST_GeometryFromText('LINESTRING (3 1, 1 3)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('LINESTRING (50 100, 50 200)')", "ST_GeometryFromText('LINESTRING (20 150, 100 150)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((4 4, 4 5, 5 5, 5 4))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Disjoint", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))')", "ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3))')"))
                .isEqualTo(false);
    }

    @Test
    public void testSTEquals()
    {
        assertThat(assertions.function("ST_Equals", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Equals", "ST_GeometryFromText('MULTIPOINT (50 100, 50 200)')", "ST_GeometryFromText('POINT (50 100)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Equals", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_GeometryFromText('LINESTRING (1 1, 1 0)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Equals", "ST_GeometryFromText('LINESTRING (0 0, 2 2)')", "ST_GeometryFromText('LINESTRING (0 0, 2 2)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Equals", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Equals", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((3 3, 3 1, 1 1, 1 3))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Equals", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))')", "ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3))')"))
                .isEqualTo(false);
    }

    @Test
    public void testSTIntersects()
    {
        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('MULTIPOINT (50 100, 50 200)')", "ST_GeometryFromText('POINT (50 100)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_GeometryFromText('LINESTRING (1 1, 1 0)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('LINESTRING (50 100, 50 200)')", "ST_GeometryFromText('LINESTRING (20 150, 100 150)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((4 4, 4 5, 5 5, 5 4))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))')", "ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('POLYGON ((16.5 54, 16.5 54.1, 16.51 54.1, 16.8 54))')", "ST_GeometryFromText('LINESTRING (16.6 53, 16.6 56)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('POLYGON ((16.5 54, 16.5 54.1, 16.51 54.1, 16.8 54))')", "ST_GeometryFromText('LINESTRING (16.6667 54.05, 16.8667 54.05)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Intersects", "ST_GeometryFromText('POLYGON ((16.5 54, 16.5 54.1, 16.51 54.1, 16.8 54))')", "ST_GeometryFromText('LINESTRING (16.6667 54.25, 16.8667 54.25)')"))
                .isEqualTo(false);
    }

    @Test
    public void testSTOverlaps()
    {
        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (50 100)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('MULTIPOINT (50 100, 50 200)')", "ST_GeometryFromText('POINT (50 100)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('LINESTRING (0 0, 0 1)')", "ST_GeometryFromText('LINESTRING (1 1, 1 0)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')", "ST_GeometryFromText('POLYGON ((3 3, 3 5, 5 5, 5 3))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')", "ST_GeometryFromText('LINESTRING (1 1, 4 4)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((4 4, 4 5, 5 5, 5 4))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Overlaps", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))')", "ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3))')"))
                .isEqualTo(true);
    }

    @Test
    public void testSTRelate()
    {
        assertThat(assertions.function("ST_Relate", "ST_GeometryFromText('LINESTRING (0 0, 3 3)')", "ST_GeometryFromText('LINESTRING (1 1, 4 1)')", "'****T****'"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Relate", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')", "'****T****'"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Relate", "ST_GeometryFromText('POLYGON ((2 0, 2 1, 3 1))')", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')", "'T********'"))
                .isEqualTo(false);
    }

    @Test
    public void testSTTouches()
    {
        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('MULTIPOINT (50 100, 50 200)')", "ST_GeometryFromText('POINT (50 100)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('LINESTRING (50 100, 50 200)')", "ST_GeometryFromText('LINESTRING (20 150, 100 150)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('POINT (1 2)')", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((4 4, 4 5, 5 5, 5 4))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('LINESTRING (0 0, 1 1)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((3 3, 3 5, 5 5, 5 3))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Touches", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))')", "ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3))')"))
                .isEqualTo(false);
    }

    @Test
    public void testSTWithin()
    {
        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('POINT (150 150)')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('POINT (50 100)')", "ST_GeometryFromText('MULTIPOINT (50 100, 50 200)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('LINESTRING (50 100, 50 200)')", "ST_GeometryFromText('LINESTRING (50 50, 50 250)')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))')", "ST_GeometryFromText('MULTILINESTRING ((3 4, 6 4), (5 0, 5 4))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('POINT (3 2)')", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('POLYGON ((1 1, 1 3, 3 3, 3 1))')", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('LINESTRING (1 1, 3 3)')", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')"))
                .isEqualTo(true);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))')", "ST_GeometryFromText('POLYGON ((0 1, 3 1, 3 3, 0 3))')"))
                .isEqualTo(false);

        assertThat(assertions.function("ST_Within", "ST_GeometryFromText('POLYGON ((1 1, 1 5, 5 5, 5 1))')", "ST_GeometryFromText('POLYGON ((0 0, 0 4, 4 4, 4 0))')"))
                .isEqualTo(false);
    }

    @Test
    public void testInvalidWKT()
    {
        assertTrinoExceptionThrownBy(assertions.function("ST_LineFromText", "'LINESTRING (0 0, 1)'")::evaluate)
                .hasMessage("Invalid WKT: LINESTRING (0 0, 1)");

        assertTrinoExceptionThrownBy(assertions.function("ST_GeometryFromText", "'POLYGON(0 0)'")::evaluate)
                .hasMessage("Invalid WKT: POLYGON(0 0)");

        assertTrinoExceptionThrownBy(assertions.function("ST_Polygon", "'POLYGON(-1 1, 1 -1)'")::evaluate)
                .hasMessage("Invalid WKT: POLYGON(-1 1, 1 -1)");
    }

    @Test
    public void testGreatCircleDistance()
    {
        assertThat(assertions.function("great_circle_distance", "36.12", "-86.67", "33.94", "-118.40"))
                .isEqualTo(2886.4489734367016);

        assertThat(assertions.function("great_circle_distance", "33.94", "-118.40", "36.12", "-86.67"))
                .isEqualTo(2886.4489734367016);

        assertThat(assertions.function("great_circle_distance", "42.3601", "-71.0589", "42.4430", "-71.2290"))
                .isEqualTo(16.73469743457383);

        assertThat(assertions.function("great_circle_distance", "36.12", "-86.67", "36.12", "-86.67"))
                .isEqualTo(0.0);

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "100", "20", "30", "40")::evaluate)
                .hasMessage("Latitude must be between -90 and 90");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "10", "20", "300", "40")::evaluate)
                .hasMessage("Latitude must be between -90 and 90");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "10", "200", "30", "40")::evaluate)
                .hasMessage("Longitude must be between -180 and 180");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "10", "20", "30", "400")::evaluate)
                .hasMessage("Longitude must be between -180 and 180");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "nan()", "-86.67", "33.94", "-118.40")::evaluate)
                .hasMessage("Latitude must be between -90 and 90");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "infinity()", "-86.67", "33.94", "-118.40")::evaluate)
                .hasMessage("Latitude must be between -90 and 90");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "36.12", "nan()", "33.94", "-118.40")::evaluate)
                .hasMessage("Longitude must be between -180 and 180");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "36.12", "infinity()", "33.94", "-118.40")::evaluate)
                .hasMessage("Longitude must be between -180 and 180");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "36.12", "-86.67", "nan()", "-118.40")::evaluate)
                .hasMessage("Latitude must be between -90 and 90");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "36.12", "-86.67", "infinity()", "-118.40")::evaluate)
                .hasMessage("Latitude must be between -90 and 90");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "36.12", "-86.67", "33.94", "nan()")::evaluate)
                .hasMessage("Longitude must be between -180 and 180");

        assertTrinoExceptionThrownBy(assertions.function("great_circle_distance", "36.12", "-86.67", "33.94", "infinity()")::evaluate)
                .hasMessage("Longitude must be between -180 and 180");
    }

    @Test
    public void testSTInteriorRings()
    {
        assertInvalidInteriorRings("POINT (2 3)", "POINT");
        assertInvalidInteriorRings("LINESTRING EMPTY", "LINE_STRING");
        assertInvalidInteriorRings("MULTIPOINT (30 20, 60 70)", "MULTI_POINT");
        assertInvalidInteriorRings("MULTILINESTRING ((1 10, 100 1000), (2 2, 1 0, 5 6))", "MULTI_LINE_STRING");
        assertInvalidInteriorRings("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((0 0, 0 2, 2 2, 2 0)))", "MULTI_POLYGON");
        assertInvalidInteriorRings("GEOMETRYCOLLECTION (POINT (1 1), POINT (2 3), LINESTRING (5 8, 13 21))", "GEOMETRY_COLLECTION");

        assertThat(assertions.function("ST_InteriorRings", "ST_GeometryFromText('POLYGON EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertInteriorRings("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))");
        assertInteriorRings("POLYGON ((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", "LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)");
        assertInteriorRings("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (3 3, 3 4, 4 4, 4 3, 3 3))",
                "LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)", "LINESTRING (3 3, 3 4, 4 4, 4 3, 3 3)");
    }

    private void assertInteriorRings(String wkt, String... expected)
    {
        assertThat(assertions.expression("transform(ST_InteriorRings(geometry), x -> ST_AsText(x))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt)))
                .hasType(new ArrayType(VARCHAR))
                .isEqualTo(ImmutableList.copyOf(expected));
    }

    private void assertInvalidInteriorRings(String wkt, String geometryType)
    {
        assertTrinoExceptionThrownBy(assertions.expression("transform(ST_InteriorRings(geometry), x -> ST_AsText(x))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                ::evaluate)
                .hasMessage("ST_InteriorRings only applies to POLYGON. Input type is: %s".formatted(geometryType));
    }

    @Test
    public void testSTNumGeometries()
    {
        assertSTNumGeometries("POINT EMPTY", 0);
        assertSTNumGeometries("LINESTRING EMPTY", 0);
        assertSTNumGeometries("POLYGON EMPTY", 0);
        assertSTNumGeometries("MULTIPOINT EMPTY", 0);
        assertSTNumGeometries("MULTILINESTRING EMPTY", 0);
        assertSTNumGeometries("MULTIPOLYGON EMPTY", 0);
        assertSTNumGeometries("GEOMETRYCOLLECTION EMPTY", 0);
        assertSTNumGeometries("POINT (1 2)", 1);
        assertSTNumGeometries("LINESTRING(77.29 29.07,77.42 29.26,77.27 29.31,77.29 29.07)", 1);
        assertSTNumGeometries("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))", 1);
        assertSTNumGeometries("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", 4);
        assertSTNumGeometries("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", 2);
        assertSTNumGeometries("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))", 2);
        assertSTNumGeometries("GEOMETRYCOLLECTION(POINT(2 3), LINESTRING (2 3, 3 4))", 2);
    }

    private void assertSTNumGeometries(String wkt, int expected)
    {
        assertThat(assertions.function("ST_NumGeometries", "ST_GeometryFromText('%s')".formatted(wkt)))
                .isEqualTo(expected);
    }

    @Test
    public void testSTUnion()
    {
        List<String> emptyWkts =
                ImmutableList.of(
                        "POINT EMPTY",
                        "MULTIPOINT EMPTY",
                        "LINESTRING EMPTY",
                        "MULTILINESTRING EMPTY",
                        "POLYGON EMPTY",
                        "MULTIPOLYGON EMPTY",
                        "GEOMETRYCOLLECTION EMPTY");
        List<String> simpleWkts =
                ImmutableList.of(
                        "POINT (1 2)",
                        "MULTIPOINT ((1 2), (3 4))",
                        "LINESTRING (0 0, 2 2, 4 4)",
                        "MULTILINESTRING ((0 0, 2 2, 4 4), (5 5, 7 7, 9 9))",
                        "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                        "MULTIPOLYGON (((1 1, 3 1, 3 3, 1 3, 1 1)), ((2 4, 6 4, 6 6, 2 6, 2 4)))",
                        "GEOMETRYCOLLECTION (LINESTRING (0 5, 5 5), POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1)))");

        // empty geometry
        for (String emptyWkt : emptyWkts) {
            for (String simpleWkt : simpleWkts) {
                assertUnion(emptyWkt, simpleWkt, simpleWkt);
            }
        }

        // self union
        for (String simpleWkt : simpleWkts) {
            assertUnion(simpleWkt, simpleWkt, simpleWkt);
        }

        // touching union
        assertUnion("POINT (1 2)", "MULTIPOINT ((1 2), (3 4))", "MULTIPOINT ((1 2), (3 4))");
        assertUnion("MULTIPOINT ((1 2))", "MULTIPOINT ((1 2), (3 4))", "MULTIPOINT ((1 2), (3 4))");
        assertUnion("LINESTRING (0 1, 1 2)", "LINESTRING (1 2, 3 4)", "LINESTRING (0 1, 1 2, 3 4)");
        assertUnion("MULTILINESTRING ((0 0, 2 2, 4 4), (5 5, 7 7, 9 9))", "MULTILINESTRING ((5 5, 7 7, 9 9), (11 11, 13 13, 15 15))", "MULTILINESTRING ((0 0, 2 2, 4 4), (5 5, 7 7, 9 9), (11 11, 13 13, 15 15))");
        assertUnion("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))", "POLYGON ((0 0, 1 0, 2 0, 2 1, 1 1, 0 1, 0 0))");
        assertUnion("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))", "MULTIPOLYGON (((1 0, 2 0, 2 1, 1 1, 1 0)))", "POLYGON ((0 0, 1 0, 2 0, 2 1, 1 1, 0 1, 0 0))");
        assertUnion("GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), POINT (1 2))", "GEOMETRYCOLLECTION (POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0)), MULTIPOINT ((1 2), (3 4)))", "GEOMETRYCOLLECTION (MULTIPOINT ((1 2), (3 4)), POLYGON ((0 0, 1 0, 2 0, 2 1, 1 1, 0 1, 0 0)))");

        // within union
        assertUnion("MULTIPOINT ((20 20), (25 25))", "POINT (25 25)", "MULTIPOINT ((20 20), (25 25))");
        assertUnion("LINESTRING (20 20, 30 30)", "POINT (25 25)", "LINESTRING (20 20, 25 25, 30 30)");
        assertUnion("LINESTRING (20 20, 30 30)", "LINESTRING (25 25, 27 27)", "LINESTRING (20 20, 25 25, 27 27, 30 30)");
        assertUnion("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))", "POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1))", "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))");
        assertUnion("MULTIPOLYGON (((0 0 , 0 2, 2 2, 2 0)), ((2 2, 2 4, 4 4, 4 2)))", "POLYGON ((2 2, 2 3, 3 3, 3 2))", "MULTIPOLYGON (((2 2, 3 2, 4 2, 4 4, 2 4, 2 3, 2 2)), ((0 0, 2 0, 2 2, 0 2, 0 0)))");
        assertUnion("GEOMETRYCOLLECTION (POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0)), MULTIPOINT ((20 20), (25 25)))", "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)), POINT (25 25))", "GEOMETRYCOLLECTION (MULTIPOINT ((20 20), (25 25)), POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0)))");

        // overlap union
        assertUnion("LINESTRING (1 1, 3 1)", "LINESTRING (2 1, 4 1)", "LINESTRING (1 1, 2 1, 3 1, 4 1)");
        assertUnion("MULTILINESTRING ((1 1, 3 1))", "MULTILINESTRING ((2 1, 4 1))", "LINESTRING (1 1, 2 1, 3 1, 4 1)");
        assertUnion("POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))", "POLYGON ((2 2, 4 2, 4 4, 2 4, 2 2))", "POLYGON ((1 1, 3 1, 3 2, 4 2, 4 4, 2 4, 2 3, 1 3, 1 1))");
        assertUnion("MULTIPOLYGON (((1 1, 3 1, 3 3, 1 3, 1 1)))", "MULTIPOLYGON (((2 2, 4 2, 4 4, 2 4, 2 2)))", "POLYGON ((1 1, 3 1, 3 2, 4 2, 4 4, 2 4, 2 3, 1 3, 1 1))");
        assertUnion("GEOMETRYCOLLECTION (POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1)), LINESTRING (1 1, 3 1))", "GEOMETRYCOLLECTION (POLYGON ((2 2, 4 2, 4 4, 2 4, 2 2)), LINESTRING (2 1, 4 1))", "GEOMETRYCOLLECTION (LINESTRING (3 1, 4 1), POLYGON ((1 1, 2 1, 3 1, 3 2, 4 2, 4 4, 2 4, 2 3, 1 3, 1 1)))");
    }

    private void assertUnion(String leftWkt, String rightWkt, String expectWkt)
    {
        assertThat(assertions.expression("ST_AsText(ST_Union(a, b))")
                .binding("a", "ST_GeometryFromText('%s')".formatted(leftWkt))
                .binding("b", "ST_GeometryFromText('%s')".formatted(rightWkt)))
                .hasType(VARCHAR)
                .isEqualTo(expectWkt);

        assertThat(assertions.expression("ST_AsText(ST_Union(a, b))")
                .binding("a", "ST_GeometryFromText('%s')".formatted(rightWkt))
                .binding("b", "ST_GeometryFromText('%s')".formatted(leftWkt)))
                .hasType(VARCHAR)
                .isEqualTo(expectWkt);
    }

    @Test
    public void testSTGeometryN()
    {
        assertSTGeometryN("POINT EMPTY", 1, null);
        assertSTGeometryN("LINESTRING EMPTY", 1, null);
        assertSTGeometryN("POLYGON EMPTY", 1, null);
        assertSTGeometryN("MULTIPOINT EMPTY", 1, null);
        assertSTGeometryN("MULTILINESTRING EMPTY", 1, null);
        assertSTGeometryN("MULTIPOLYGON EMPTY", 1, null);
        assertSTGeometryN("POINT EMPTY", 0, null);
        assertSTGeometryN("LINESTRING EMPTY", 0, null);
        assertSTGeometryN("POLYGON EMPTY", 0, null);
        assertSTGeometryN("MULTIPOINT EMPTY", 0, null);
        assertSTGeometryN("MULTILINESTRING EMPTY", 0, null);
        assertSTGeometryN("MULTIPOLYGON EMPTY", 0, null);
        assertSTGeometryN("POINT (1 2)", 1, "POINT (1 2)");
        assertSTGeometryN("POINT (1 2)", -1, null);
        assertSTGeometryN("POINT (1 2)", 2, null);
        assertSTGeometryN("LINESTRING(77.29 29.07, 77.42 29.26, 77.27 29.31, 77.29 29.07)", 1, "LINESTRING (77.29 29.07, 77.42 29.26, 77.27 29.31, 77.29 29.07)");
        assertSTGeometryN("LINESTRING(77.29 29.07, 77.42 29.26, 77.27 29.31, 77.29 29.07)", 2, null);
        assertSTGeometryN("LINESTRING(77.29 29.07, 77.42 29.26, 77.27 29.31, 77.29 29.07)", -1, null);
        assertSTGeometryN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))");
        assertSTGeometryN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 2, null);
        assertSTGeometryN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", -1, null);
        assertSTGeometryN("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", 1, "POINT (1 2)");
        assertSTGeometryN("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", 2, "POINT (2 4)");
        assertSTGeometryN("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", 0, null);
        assertSTGeometryN("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", 5, null);
        assertSTGeometryN("MULTIPOINT (1 2, 2 4, 3 6, 4 8)", -1, null);
        assertSTGeometryN("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", 1, "LINESTRING (1 1, 5 1)");
        assertSTGeometryN("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", 2, "LINESTRING (2 4, 4 4)");
        assertSTGeometryN("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", 0, null);
        assertSTGeometryN("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", 3, null);
        assertSTGeometryN("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", -1, null);
        assertSTGeometryN("MULTIPOLYGON (((1 1, 3 1, 3 3, 1 3, 1 1)), ((2 4, 6 4, 6 6, 2 6, 2 4)))", 1, "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))");
        assertSTGeometryN("MULTIPOLYGON (((1 1, 3 1, 3 3, 1 3, 1 1)), ((2 4, 6 4, 6 6, 2 6, 2 4)))", 2, "POLYGON ((2 4, 6 4, 6 6, 2 6, 2 4))");
        assertSTGeometryN("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))", 0, null);
        assertSTGeometryN("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))", 3, null);
        assertSTGeometryN("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))", -1, null);
        assertSTGeometryN("GEOMETRYCOLLECTION(POINT(2 3), LINESTRING (2 3, 3 4))", 1, "POINT (2 3)");
        assertSTGeometryN("GEOMETRYCOLLECTION(POINT(2 3), LINESTRING (2 3, 3 4))", 2, "LINESTRING (2 3, 3 4)");
        assertSTGeometryN("GEOMETRYCOLLECTION(POINT(2 3), LINESTRING (2 3, 3 4))", 3, null);
        assertSTGeometryN("GEOMETRYCOLLECTION(POINT(2 3), LINESTRING (2 3, 3 4))", 0, null);
        assertSTGeometryN("GEOMETRYCOLLECTION(POINT(2 3), LINESTRING (2 3, 3 4))", -1, null);
    }

    private void assertSTGeometryN(String wkt, int index, String expected)
    {
        assertThat(assertions.expression("ST_AsText(ST_GeometryN(geometry, index))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                .binding("index", Integer.toString(index)))
                .hasType(VARCHAR)
                .isEqualTo(expected);
    }

    @Test
    public void testSTLineString()
    {
        // General case, 2+ points
        assertThat(assertions.function("ST_LineString", "array[ST_Point(1,2), ST_Point(3,4)]"))
                .hasType(GEOMETRY)
                .isEqualTo("LINESTRING (1 2, 3 4)");

        assertThat(assertions.function("ST_LineString", "array[ST_Point(1,2), ST_Point(3,4), ST_Point(5, 6)]"))
                .hasType(GEOMETRY)
                .isEqualTo("LINESTRING (1 2, 3 4, 5 6)");

        assertThat(assertions.function("ST_LineString", "array[ST_Point(1,2), ST_Point(3,4), ST_Point(5,6), ST_Point(7,8)]"))
                .hasType(GEOMETRY)
                .isEqualTo("LINESTRING (1 2, 3 4, 5 6, 7 8)");

        // Other ways of creating points
        assertThat(assertions.function("ST_LineString", "array[ST_GeometryFromText('POINT (1 2)'), ST_GeometryFromText('POINT (3 4)')]"))
                .hasType(GEOMETRY)
                .isEqualTo("LINESTRING (1 2, 3 4)");

        // Duplicate consecutive points throws exception
        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(1, 2), ST_Point(1, 2)]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: consecutive duplicate points at index 2");

        assertThat(assertions.function("ST_LineString", "array[ST_Point(1, 2), ST_Point(3, 4), ST_Point(1, 2)]"))
                .hasType(GEOMETRY)
                .isEqualTo("LINESTRING (1 2, 3 4, 1 2)");

        // Single point
        assertThat(assertions.function("ST_LineString", "array[ST_Point(9,10)]"))
                .hasType(GEOMETRY)
                .isEqualTo("LINESTRING EMPTY");

        // Zero points
        assertThat(assertions.function("ST_LineString", "array[]"))
                .hasType(GEOMETRY)
                .isEqualTo("LINESTRING EMPTY");

        // Only points can be passed
        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(7,8), ST_GeometryFromText('LINESTRING (1 2, 3 4)')]")::evaluate)
                .hasMessage("ST_LineString takes only an array of valid points, LineString was passed");

        // Nulls points are invalid
        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[NULL]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: null point at index 1");

        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(1,2), NULL]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: null point at index 2");

        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(1, 2), NULL, ST_Point(3, 4)]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: null point at index 2");

        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(1, 2), NULL, ST_Point(3, 4), NULL]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: null point at index 2");

        // Empty points are invalid
        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_GeometryFromText('POINT EMPTY')]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: empty point at index 1");

        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(1,2), ST_GeometryFromText('POINT EMPTY')]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: empty point at index 2");

        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(1,2), ST_GeometryFromText('POINT EMPTY'), ST_Point(3,4)]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: empty point at index 2");

        assertTrinoExceptionThrownBy(assertions.function("ST_LineString", "array[ST_Point(1,2), ST_GeometryFromText('POINT EMPTY'), ST_Point(3,4), ST_GeometryFromText('POINT EMPTY')]")::evaluate)
                .hasMessage("Invalid input to ST_LineString: empty point at index 2");
    }

    @Test
    public void testMultiPoint()
    {
        // General case, 2+ points
        assertMultiPoint("MULTIPOINT ((1 2), (3 4))", "POINT (1 2)", "POINT (3 4)");
        assertMultiPoint("MULTIPOINT ((1 2), (3 4), (5 6))", "POINT (1 2)", "POINT (3 4)", "POINT (5 6)");
        assertMultiPoint("MULTIPOINT ((1 2), (3 4), (5 6), (7 8))", "POINT (1 2)", "POINT (3 4)", "POINT (5 6)", "POINT (7 8)");

        // Duplicate points work
        assertMultiPoint("MULTIPOINT ((1 2), (1 2))", "POINT (1 2)", "POINT (1 2)");
        assertMultiPoint("MULTIPOINT ((1 2), (3 4), (1 2))", "POINT (1 2)", "POINT (3 4)", "POINT (1 2)");

        // Single point
        assertMultiPoint("MULTIPOINT ((1 2))", "POINT (1 2)");

        // Empty array
        assertThat(assertions.function("ST_MultiPoint", "array[]"))
                .isNull(GEOMETRY);

        // Only points can be passed
        assertInvalidMultiPoint("geometry is not a point: LineString at index 2", "POINT (7 8)", "LINESTRING (1 2, 3 4)");

        // Null point raises exception
        assertTrinoExceptionThrownBy(assertions.function("ST_MultiPoint", "array[null]")::evaluate)
                .hasMessage("Invalid input to ST_MultiPoint: null at index 1");

        assertInvalidMultiPoint("null at index 3", "POINT (1 2)", "POINT (1 2)", null);
        assertInvalidMultiPoint("null at index 2", "POINT (1 2)", null, "POINT (3 4)");
        assertInvalidMultiPoint("null at index 2", "POINT (1 2)", null, "POINT (3 4)", null);

        // Empty point raises exception
        assertInvalidMultiPoint("empty point at index 1", "POINT EMPTY");
        assertInvalidMultiPoint("empty point at index 2", "POINT (1 2)", "POINT EMPTY");
    }

    private void assertMultiPoint(String expectedWkt, String... pointWkts)
    {
        assertThat(assertions.expression("ST_MultiPoint(a)")
                .binding("a", "array[%s]".formatted(Arrays.stream(pointWkts)
                        .map(wkt -> wkt == null ? "null" : "ST_GeometryFromText('%s')".formatted(wkt))
                        .collect(Collectors.joining(",")))))
                .hasType(GEOMETRY)
                .isEqualTo(expectedWkt);
    }

    private void assertInvalidMultiPoint(String errorMessage, String... pointWkts)
    {
        assertTrinoExceptionThrownBy(() -> assertions.expression("ST_MultiPoint(a)")
                .binding("a", "array[%s]".formatted(Arrays.stream(pointWkts)
                        .map(wkt -> wkt == null ? "null" : "ST_GeometryFromText('%s')".formatted(wkt))
                        .collect(Collectors.joining(","))))
                .evaluate())
                .hasMessage("Invalid input to ST_MultiPoint: %s".formatted(errorMessage));
    }

    @Test
    public void testSTPointN()
    {
        assertPointN("LINESTRING(1 2, 3 4, 5 6, 7 8)", 1, "POINT (1 2)");
        assertPointN("LINESTRING(1 2, 3 4, 5 6, 7 8)", 3, "POINT (5 6)");
        assertPointN("LINESTRING(1 2, 3 4, 5 6, 7 8)", 10, null);
        assertPointN("LINESTRING(1 2, 3 4, 5 6, 7 8)", 0, null);
        assertPointN("LINESTRING(1 2, 3 4, 5 6, 7 8)", -1, null);

        assertInvalidPointN("POINT (1 2)", "POINT");
        assertInvalidPointN("MULTIPOINT (1 1, 2 2)", "MULTI_POINT");
        assertInvalidPointN("MULTILINESTRING ((1 1, 2 2), (3 3, 4 4))", "MULTI_LINE_STRING");
        assertInvalidPointN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POLYGON");
        assertInvalidPointN("MULTIPOLYGON (((1 1, 1 4, 4 4, 4 1)), ((1 1, 1 4, 4 4, 4 1)))", "MULTI_POLYGON");
        assertInvalidPointN("GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6, 7 10))", "GEOMETRY_COLLECTION");
    }

    private void assertPointN(String wkt, int index, String expected)
    {
        assertThat(assertions.expression("ST_AsText(ST_PointN(geometry, index))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                .binding("index", Integer.toString(index)))
                .hasType(VARCHAR)
                .isEqualTo(expected);
    }

    private void assertInvalidPointN(String wkt, String type)
    {
        assertTrinoExceptionThrownBy(() -> assertions.expression("ST_PointN(geometry, 1)")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                .evaluate())
                .hasMessage("ST_PointN only applies to LINE_STRING. Input type is: %s".formatted(type));
    }

    @Test
    public void testSTGeometries()
    {
        assertThat(assertions.function("ST_Geometries", "ST_GeometryFromText('POINT EMPTY')"))
                .isNull(new ArrayType(GEOMETRY));

        assertSTGeometries("POINT (1 5)", "POINT (1 5)");
        assertSTGeometries("LINESTRING (77.29 29.07, 77.42 29.26, 77.27 29.31, 77.29 29.07)", "LINESTRING (77.29 29.07, 77.42 29.26, 77.27 29.31, 77.29 29.07)");
        assertSTGeometries("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))");
        assertSTGeometries("MULTIPOINT (1 2, 4 8, 16 32)", "POINT (1 2)", "POINT (4 8)", "POINT (16 32)");
        assertSTGeometries("MULTILINESTRING ((1 1, 2 2))", "LINESTRING (1 1, 2 2)");
        assertSTGeometries("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((1 1, 3 1, 3 3, 1 3, 1 1)))",
                "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))");
        assertSTGeometries("GEOMETRYCOLLECTION (POINT (2 3), LINESTRING (2 3, 3 4))", "POINT (2 3)", "LINESTRING (2 3, 3 4)");
    }

    private void assertSTGeometries(String wkt, String... expected)
    {
        assertThat(assertions.expression("transform(ST_Geometries(geometry), x -> ST_AsText(x))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt)))
                .hasType(new ArrayType(VARCHAR))
                .isEqualTo(ImmutableList.copyOf(expected));
    }

    @Test
    public void testSTInteriorRingN()
    {
        assertInvalidInteriorRingN("POINT EMPTY", 0, "POINT");
        assertInvalidInteriorRingN("LINESTRING (1 2, 2 3, 3 4)", 1, "LINE_STRING");
        assertInvalidInteriorRingN("MULTIPOINT (1 1, 2 3, 5 8)", -1, "MULTI_POINT");
        assertInvalidInteriorRingN("MULTILINESTRING ((2 4, 4 2), (3 5, 5 3))", 0, "MULTI_LINE_STRING");
        assertInvalidInteriorRingN("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1)), ((2 4, 2 6, 6 6, 6 4)))", 2, "MULTI_POLYGON");
        assertInvalidInteriorRingN("GEOMETRYCOLLECTION (POINT (2 2), POINT (10 20))", 1, "GEOMETRY_COLLECTION");

        assertInteriorRingN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1, null);
        assertInteriorRingN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 2, null);
        assertInteriorRingN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", -1, null);
        assertInteriorRingN("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 0, null);
        assertInteriorRingN("POLYGON ((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", 1, "LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)");
        assertInteriorRingN("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (3 3, 3 4, 4 4, 4 3, 3 3))", 2, "LINESTRING (3 3, 3 4, 4 4, 4 3, 3 3)");
    }

    private void assertInteriorRingN(String wkt, int index, String expected)
    {
        assertThat(assertions.expression("ST_AsText(ST_InteriorRingN(geometry, index))")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                .binding("index", Integer.toString(index)))
                .hasType(VARCHAR)
                .isEqualTo(expected);
    }

    private void assertInvalidInteriorRingN(String wkt, int index, String geometryType)
    {
        assertTrinoExceptionThrownBy(() -> assertions.expression("ST_InteriorRingN(geometry, index)")
                .binding("geometry", "ST_GeometryFromText('%s')".formatted(wkt))
                .binding("index", Integer.toString(index))
                .evaluate())
                .hasMessage("ST_InteriorRingN only applies to POLYGON. Input type is: %s".formatted(geometryType));
    }

    @Test
    public void testSTGeometryType()
    {
        assertThat(assertions.function("ST_GeometryType", "ST_Point(1, 4)"))
                .hasType(VARCHAR)
                .isEqualTo("ST_Point");

        assertThat(assertions.function("ST_GeometryType", "ST_GeometryFromText('LINESTRING (1 1, 2 2)')"))
                .hasType(VARCHAR)
                .isEqualTo("ST_LineString");

        assertThat(assertions.function("ST_GeometryType", "ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1))')"))
                .hasType(VARCHAR)
                .isEqualTo("ST_Polygon");

        assertThat(assertions.function("ST_GeometryType", "ST_GeometryFromText('MULTIPOINT (1 1, 2 2)')"))
                .hasType(VARCHAR)
                .isEqualTo("ST_MultiPoint");

        assertThat(assertions.function("ST_GeometryType", "ST_GeometryFromText('MULTILINESTRING ((1 1, 2 2), (3 3, 4 4))')"))
                .hasType(VARCHAR)
                .isEqualTo("ST_MultiLineString");

        assertThat(assertions.function("ST_GeometryType", "ST_GeometryFromText('MULTIPOLYGON (((1 1, 1 4, 4 4, 4 1)), ((1 1, 1 4, 4 4, 4 1)))')"))
                .hasType(VARCHAR)
                .isEqualTo("ST_MultiPolygon");

        assertThat(assertions.function("ST_GeometryType", "ST_GeometryFromText('GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6, 7 10))')"))
                .hasType(VARCHAR)
                .isEqualTo("ST_GeomCollection");

        assertThat(assertions.function("ST_GeometryType", "ST_Envelope(ST_GeometryFromText('LINESTRING (1 1, 2 2)'))"))
                .hasType(VARCHAR)
                .isEqualTo("ST_Polygon");
    }

    @Test
    public void testSTGeometryFromBinary()
    {
        assertThat(assertions.function("ST_GeomFromBinary", "null"))
                .isNull(GEOMETRY);

        // empty geometries
        assertGeomFromBinary("POINT EMPTY");
        assertGeomFromBinary("MULTIPOINT EMPTY");
        assertGeomFromBinary("LINESTRING EMPTY");
        assertGeomFromBinary("MULTILINESTRING EMPTY");
        assertGeomFromBinary("POLYGON EMPTY");
        assertGeomFromBinary("MULTIPOLYGON EMPTY");
        assertGeomFromBinary("GEOMETRYCOLLECTION EMPTY");

        // valid nonempty geometries
        assertGeomFromBinary("POINT (1 2)");
        assertGeomFromBinary("MULTIPOINT ((1 2), (3 4))");
        assertGeomFromBinary("LINESTRING (0 0, 1 2, 3 4)");
        assertGeomFromBinary("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))");
        assertGeomFromBinary("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))");
        assertGeomFromBinary("POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))");
        assertGeomFromBinary("MULTIPOLYGON (((1 1, 3 1, 3 3, 1 3, 1 1)), ((2 4, 6 4, 6 6, 2 6, 2 4)))");
        assertGeomFromBinary("GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (0 0, 1 2, 3 4), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))");

        // The EWKB representation of "SRID=4326;POINT (1 1)".
        assertThat(assertions.expression("ST_AsText(ST_GeomFromBinary(wkb))")
                .binding("wkb", "x'0101000020E6100000000000000000F03F000000000000F03F'"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (1 1)");

        // array of geometries
        assertThat(assertions.expression("transform(a, wkb -> ST_AsText(ST_GeomFromBinary(wkb)))")
                .binding("a", "ARRAY[ST_AsBinary(ST_Point(1, 2)), ST_AsBinary(ST_Point(3, 4))]"))
                .hasType(new ArrayType(VARCHAR))
                .isEqualTo(ImmutableList.of("POINT (1 2)", "POINT (3 4)"));

        // invalid geometries
        assertGeomFromBinary("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))");
        assertGeomFromBinary("LINESTRING (0 0, 0 1, 0 1, 1 1, 1 0, 0 0)");

        // invalid binary
        assertTrinoExceptionThrownBy(assertions.function("ST_GeomFromBinary", "from_hex('deadbeef')")::evaluate)
                .hasMessage("Invalid WKB");
    }

    private void assertGeomFromBinary(String wkt)
    {
        assertThat(assertions.expression("ST_AsText(ST_GeomFromBinary(geometry))")
                .binding("geometry", "ST_AsBinary(ST_GeometryFromText('%s'))".formatted(wkt)))
                .hasType(VARCHAR)
                .isEqualTo(wkt);
    }

    @Test
    public void testGeometryFromHadoopShape()
    {
        assertThat(assertions.function("geometry_from_hadoop_shape", "null"))
                .isNull(GEOMETRY);

        // empty geometries
        assertGeometryFromHadoopShape("000000000101000000FFFFFFFFFFFFEFFFFFFFFFFFFFFFEFFF", "POINT EMPTY");
        assertGeometryFromHadoopShape("000000000203000000000000000000F87F000000000000F87F000000000000F87F000000000000F87F0000000000000000", "LINESTRING EMPTY");
        assertGeometryFromHadoopShape("000000000305000000000000000000F87F000000000000F87F000000000000F87F000000000000F87F0000000000000000", "POLYGON EMPTY");
        assertGeometryFromHadoopShape("000000000408000000000000000000F87F000000000000F87F000000000000F87F000000000000F87F00000000", "MULTIPOINT EMPTY");
        assertGeometryFromHadoopShape("000000000503000000000000000000F87F000000000000F87F000000000000F87F000000000000F87F0000000000000000", "MULTILINESTRING EMPTY");
        assertGeometryFromHadoopShape("000000000605000000000000000000F87F000000000000F87F000000000000F87F000000000000F87F0000000000000000", "MULTIPOLYGON EMPTY");

        // valid nonempty geometries
        assertGeometryFromHadoopShape("000000000101000000000000000000F03F0000000000000040", "POINT (1 2)");
        assertGeometryFromHadoopShape("000000000203000000000000000000000000000000000000000000000000000840000000000000104001000000030000000000000000000000000000000000000000000000000000000000F03F000000000000004000000000000008400000000000001040", "LINESTRING (0 0, 1 2, 3 4)");
        assertGeometryFromHadoopShape("00000000030500000000000000000000000000000000000000000000000000F03F000000000000F03F010000000500000000000000000000000000000000000000000000000000000000000000000000000000F03F000000000000F03F000000000000F03F000000000000F03F000000000000000000000000000000000000000000000000", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))");
        assertGeometryFromHadoopShape("000000000408000000000000000000F03F00000000000000400000000000000840000000000000104002000000000000000000F03F000000000000004000000000000008400000000000001040", "MULTIPOINT ((1 2), (3 4))");
        assertGeometryFromHadoopShape("000000000503000000000000000000F03F000000000000F03F0000000000001440000000000000104002000000040000000000000002000000000000000000F03F000000000000F03F0000000000001440000000000000F03F0000000000000040000000000000104000000000000010400000000000001040", "MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))");
        assertGeometryFromHadoopShape("000000000605000000000000000000F03F000000000000F03F00000000000018400000000000001840020000000A0000000000000005000000000000000000F03F000000000000F03F000000000000F03F0000000000000840000000000000084000000000000008400000000000000840000000000000F03F000000000000F03F000000000000F03F0000000000000040000000000000104000000000000000400000000000001840000000000000184000000000000018400000000000001840000000000000104000000000000000400000000000001040", "MULTIPOLYGON (((1 1, 3 1, 3 3, 1 3, 1 1)), ((2 4, 6 4, 6 6, 2 6, 2 4)))");

        // given hadoop shape is too short
        assertTrinoExceptionThrownBy(assertions.function("geometry_from_hadoop_shape", "from_hex('1234')")::evaluate)
                .hasMessage("Hadoop shape input is too short");

        // hadoop shape type invalid
        assertTrinoExceptionThrownBy(assertions.function("geometry_from_hadoop_shape", "from_hex('000000000701000000FFFFFFFFFFFFEFFFFFFFFFFFFFFFEFFF')")::evaluate)
                .hasMessage("Invalid Hadoop shape type: 7");

        assertTrinoExceptionThrownBy(assertions.function("geometry_from_hadoop_shape", "from_hex('00000000FF01000000FFFFFFFFFFFFEFFFFFFFFFFFFFFFEFFF')")::evaluate)
                .hasMessage("Invalid Hadoop shape type: -1");

        // esri shape invalid
        assertTrinoExceptionThrownBy(assertions.function("geometry_from_hadoop_shape", "from_hex('000000000101000000FFFFFFFFFFFFEFFFFFFFFFFFFFFFEF')")::evaluate)
                .hasMessage("Invalid Hadoop shape");

        // shape type is invalid for given shape
        assertTrinoExceptionThrownBy(assertions.function("geometry_from_hadoop_shape", "from_hex('000000000501000000000000000000F03F0000000000000040')")::evaluate)
                .hasMessage("Invalid Hadoop shape");
    }

    private void assertGeometryFromHadoopShape(String hadoopHex, String expectedWkt)
    {
        assertThat(assertions.expression("ST_AsText(geometry_from_hadoop_shape(geometry))")
                .binding("geometry", "from_hex('%s')".formatted(hadoopHex)))
                .hasType(VARCHAR)
                .isEqualTo(expectedWkt);
    }

    @Test
    public void testSphericalGeographyJsonConversion()
    {
        // empty geometries should return empty
        // empty geometries are represented by an empty JSON array in GeoJSON
        assertGeographyToAndFromJson("POINT EMPTY");
        assertGeographyToAndFromJson("LINESTRING EMPTY");
        assertGeographyToAndFromJson("POLYGON EMPTY");
        assertGeographyToAndFromJson("MULTIPOINT EMPTY");
        assertGeographyToAndFromJson("MULTILINESTRING EMPTY");
        assertGeographyToAndFromJson("MULTIPOLYGON EMPTY");
        assertGeographyToAndFromJson("GEOMETRYCOLLECTION EMPTY");

        // valid nonempty geometries should return as is.
        assertGeographyToAndFromJson("POINT (1 2)");
        assertGeographyToAndFromJson("MULTIPOINT ((1 2), (3 4))");
        assertGeographyToAndFromJson("LINESTRING (0 0, 1 2, 3 4)");
        assertGeographyToAndFromJson("MULTILINESTRING (" +
                "(1 1, 5 1), " +
                "(2 4, 4 4))");
        assertGeographyToAndFromJson("POLYGON (" +
                "(0 0, 1 0, 1 1, 0 1, 0 0))");
        assertGeographyToAndFromJson("POLYGON (" +
                "(0 0, 3 0, 3 3, 0 3, 0 0), " +
                "(1 1, 1 2, 2 2, 2 1, 1 1))");
        assertGeographyToAndFromJson("MULTIPOLYGON (" +
                "((1 1, 3 1, 3 3, 1 3, 1 1)), " +
                "((2 4, 6 4, 6 6, 2 6, 2 4)))");
        assertGeographyToAndFromJson("GEOMETRYCOLLECTION (" +
                "POINT (1 2), " +
                "LINESTRING (0 0, 1 2, 3 4), " +
                "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))");

        // invalid geometries should return as is.
        assertGeographyToAndFromJson("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))");
        assertGeographyToAndFromJson("LINESTRING (0 0, 0 1, 0 1, 1 1, 1 0, 0 0)");
        assertGeographyToAndFromJson("LINESTRING (0 0, 1 1, 1 0, 0 1)");

        // extra properties are stripped from JSON
        assertValidGeometryJson("{\"type\":\"Point\", \"coordinates\":[0,0], \"mykey\":\"myvalue\"}", "POINT (0 0)");

        // explicit JSON test cases should valid but return empty
        assertValidGeometryJson("{\"type\":\"Point\", \"coordinates\":[]}", "POINT EMPTY");
        assertValidGeometryJson("{\"type\":\"LineString\", \"coordinates\":[]}", "LINESTRING EMPTY");
        assertValidGeometryJson("{\"type\":\"Polygon\", \"coordinates\":[]}", "POLYGON EMPTY");
        assertValidGeometryJson("{\"type\":\"MultiPoint\", \"coordinates\":[]}", "MULTIPOINT EMPTY");
        assertValidGeometryJson("{\"type\":\"MultiPolygon\", \"coordinates\":[]}", "MULTIPOLYGON EMPTY");
        assertValidGeometryJson(
                "{\"type\":\"MultiLineString\", \"coordinates\":[[[0.0,0.0],[1,10]],[[10,10],[20,30]],[[123,123],[456,789]]]}",
                "MULTILINESTRING ((0 0, 1 10), (10 10, 20 30), (123 123, 456 789))");
        assertValidGeometryJson("{\"type\":\"Point\"}", "POINT EMPTY");
        assertValidGeometryJson("{\"type\":\"LineString\",\"coordinates\":null}", "LINESTRING EMPTY");
        assertValidGeometryJson("{\"type\":\"MultiPoint\",\"invalidField\":[[10,10],[20,30]]}", "MULTIPOINT EMPTY");
        assertValidGeometryJson("{\"type\":\"FeatureCollection\",\"features\":[]}", "GEOMETRYCOLLECTION EMPTY");

        // Valid JSON with invalid Geometry definition
        assertInvalidGeometryJson("{ \"data\": {\"type\":\"Point\",\"coordinates\":[0,0]}}",
                "Invalid GeoJSON: Could not parse Geometry from Json string.  No 'type' property found.");
        assertInvalidGeometryJson("{\"type\":\"Feature\",\"geometry\":[],\"property\":\"foo\"}",
                "Invalid GeoJSON: Could not parse Feature from GeoJson string.");
        assertInvalidGeometryJson("{\"coordinates\":[[[0.0,0.0],[1,10]],[[10,10],[20,30]],[[123,123],[456,789]]]}",
                "Invalid GeoJSON: Could not parse Geometry from Json string.  No 'type' property found.");

        // Invalid JSON
        assertInvalidGeometryJson("{\"type\":\"MultiPoint\",\"crashMe\"}",
                "Invalid GeoJSON: Unexpected token RIGHT BRACE(}) at position 30.");
    }

    private void assertGeographyToAndFromJson(String wkt)
    {
        assertThat(assertions.function("ST_AsText", "to_geometry(from_geojson_geometry(to_geojson_geometry(to_spherical_geography(ST_GeometryFromText('%s')))))".formatted(wkt)))
                .hasType(VARCHAR)
                .isEqualTo(wkt);
    }

    private void assertValidGeometryJson(String json, String wkt)
    {
        assertThat(assertions.function("ST_AsText", "to_geometry(from_geojson_geometry('%s'))".formatted(json)))
                .hasType(VARCHAR)
                .isEqualTo(wkt);
    }

    private void assertInvalidGeometryJson(String json, String message)
    {
        assertTrinoExceptionThrownBy(assertions.function("from_geojson_geometry", "'%s'".formatted(json))::evaluate)
                .hasMessage(message);
    }

    @Test
    public void testGeometryJsonConversion()
    {
        // empty geometries should return empty
        // empty geometries are represented by an empty JSON array in GeoJSON
        assertGeometryToAndFromJson("POINT EMPTY");
        assertGeometryToAndFromJson("LINESTRING EMPTY");
        assertGeometryToAndFromJson("POLYGON EMPTY");
        assertGeometryToAndFromJson("MULTIPOINT EMPTY");
        assertGeometryToAndFromJson("MULTILINESTRING EMPTY");
        assertGeometryToAndFromJson("MULTIPOLYGON EMPTY");
        assertGeometryToAndFromJson("GEOMETRYCOLLECTION EMPTY");

        // valid nonempty geometries should return as is.
        assertGeometryToAndFromJson("POINT (1 2)");
        assertGeometryToAndFromJson("MULTIPOINT ((1 2), (3 4))");
        assertGeometryToAndFromJson("LINESTRING (0 0, 1 2, 3 4)");
        assertGeometryToAndFromJson("MULTILINESTRING (" +
                "(1 1, 5 1), " +
                "(2 4, 4 4))");
        assertGeometryToAndFromJson("POLYGON (" +
                "(0 0, 1 0, 1 1, 0 1, 0 0))");
        assertGeometryToAndFromJson("POLYGON (" +
                "(0 0, 3 0, 3 3, 0 3, 0 0), " +
                "(1 1, 1 2, 2 2, 2 1, 1 1))");
        assertGeometryToAndFromJson("MULTIPOLYGON (" +
                "((1 1, 3 1, 3 3, 1 3, 1 1)), " +
                "((2 4, 6 4, 6 6, 2 6, 2 4)))");
        assertGeometryToAndFromJson("GEOMETRYCOLLECTION (" +
                "POINT (1 2), " +
                "LINESTRING (0 0, 1 2, 3 4), " +
                "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))");

        // invalid geometries should return as is.
        assertGeometryToAndFromJson("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))");
        assertGeometryToAndFromJson("LINESTRING (0 0, 0 1, 0 1, 1 1, 1 0, 0 0)");
        assertGeometryToAndFromJson("LINESTRING (0 0, 1 1, 1 0, 0 1)");
    }

    private void assertGeometryToAndFromJson(String wkt)
    {
        assertThat(assertions.function("ST_AsText", "to_geometry(from_geojson_geometry(to_geojson_geometry(ST_GeometryFromText('%s'))))".formatted(wkt)))
                .hasType(VARCHAR)
                .isEqualTo(wkt);
    }

    @Test
    public void testSTGeomFromKML()
    {
        assertThat(assertions.expression("ST_AsText(ST_GeomFromKML(geometry))")
                .binding("geometry", "'<Point><coordinates>-2,2</coordinates></Point>'"))
                .hasType(VARCHAR)
                .isEqualTo("POINT (-2 2)");

        assertTrinoExceptionThrownBy(assertions.function("ST_GeomFromKML", "'<Point>'")::evaluate)
                .hasMessage("Invalid KML: <Point>");
    }
}
